nip46_work.rs (47382B)
1 //! Transaction-free NIP-46 decryption, method admission, and provider work. 2 3 use core::fmt; 4 use std::error::Error; 5 6 use nostr::UnsignedEvent as NostrUnsignedEvent; 7 use radroots_nostr_connect::{ 8 message::{Request, RequestMessage, UnsignedEvent as ConnectUnsignedEvent}, 9 server::required_permission, 10 }; 11 use sha2::{Digest, Sha256}; 12 13 use crate::{ 14 MycConnectionAdmissionPolicy, MycConnectionAdmissionRequest, MycConnectionNonce, 15 MycConnectionPermission, MycConnectionPermissionSet, MycConnectionPolicyGeneration, 16 MycConnectionRecord, MycConnectionStatus, MycConnectionTimeUnixMs, MycNip46AdmissionLimits, 17 MycNip46EncryptionContext, MycProviderBinding, MycProviderContract, MycProviderCorrelationId, 18 MycProviderDeadlineUnixMs, MycProviderNip44Version, MycProviderOperation, 19 MycProviderOperationId, MycProviderOperationInput, MycProviderPublicIdentity, MycProviderRole, 20 MycRateRelayId, MycReplayBoundNip46Request, MycRequestReceivedAtUnixMs, 21 MycSignerOperationNonce, MycSignerRequest, MycSignerRequestMethod, MycSignerRequestRecord, 22 MycVerifiedNip46Event, MycVerifiedProviderResponse, admit_myc_nip46_request, 23 bind_myc_nip46_replay, verify_myc_nip46_request, 24 }; 25 26 const DECRYPT_OPERATION_ID_DOMAIN: &[u8] = b"radroots.myc.nip46.decrypt.operation.v1\0"; 27 const DECRYPT_CORRELATION_ID_DOMAIN: &[u8] = b"radroots.myc.nip46.decrypt.correlation.v1\0"; 28 29 /// Stable source-free Step 145 failure classification. 30 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 31 pub enum MycNip46WorkErrorKind { 32 InvalidBinding, 33 DecryptionRejected, 34 UnsupportedMethod, 35 PermissionDenied, 36 ConnectionExpired, 37 ProviderWorkRejected, 38 } 39 40 impl MycNip46WorkErrorKind { 41 /// Returns the stable machine-facing safe code. 42 #[must_use] 43 pub const fn code(self) -> &'static str { 44 match self { 45 Self::InvalidBinding => "nip46_work_binding_invalid", 46 Self::DecryptionRejected => "nip46_decryption_rejected", 47 Self::UnsupportedMethod => "nip46_method_unsupported", 48 Self::PermissionDenied => "nip46_permission_denied", 49 Self::ConnectionExpired => "nip46_connection_inactive", 50 Self::ProviderWorkRejected => "nip46_provider_work_rejected", 51 } 52 } 53 54 const fn message(self) -> &'static str { 55 match self { 56 Self::InvalidBinding => "NIP-46 work binding is invalid", 57 Self::DecryptionRejected => "NIP-46 decrypted request was rejected", 58 Self::UnsupportedMethod => "NIP-46 method is unsupported", 59 Self::PermissionDenied => "NIP-46 permission was denied", 60 Self::ConnectionExpired => "NIP-46 connection is not active", 61 Self::ProviderWorkRejected => "NIP-46 provider work was rejected", 62 } 63 } 64 } 65 66 /// One redacted source-free Step 145 failure. 67 #[derive(Clone, Copy, PartialEq, Eq)] 68 pub struct MycNip46WorkError { 69 kind: MycNip46WorkErrorKind, 70 } 71 72 impl MycNip46WorkError { 73 /// Returns the stable failure classification. 74 #[must_use] 75 pub const fn kind(self) -> MycNip46WorkErrorKind { 76 self.kind 77 } 78 79 /// Returns the stable machine-facing safe code. 80 #[must_use] 81 pub const fn code(self) -> &'static str { 82 self.kind.code() 83 } 84 } 85 86 impl fmt::Debug for MycNip46WorkError { 87 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 88 formatter 89 .debug_struct("MycNip46WorkError") 90 .field("kind", &self.kind) 91 .finish() 92 } 93 } 94 95 impl fmt::Display for MycNip46WorkError { 96 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 97 formatter.write_str(self.kind.message()) 98 } 99 } 100 101 impl Error for MycNip46WorkError {} 102 103 const fn work_error(kind: MycNip46WorkErrorKind) -> MycNip46WorkError { 104 MycNip46WorkError { kind } 105 } 106 107 /// Provider-bound work that must complete before plaintext request admission. 108 pub struct MycNip46DecryptWork { 109 event: MycVerifiedNip46Event, 110 operation: MycProviderOperation, 111 } 112 113 impl MycNip46DecryptWork { 114 /// Returns the exact transport-provider operation for external execution. 115 #[must_use] 116 pub const fn operation(&self) -> &MycProviderOperation { 117 &self.operation 118 } 119 120 /// Binds an independently verified provider result to the exact event. 121 pub fn complete( 122 self, 123 response: MycVerifiedProviderResponse, 124 limits: MycNip46AdmissionLimits, 125 ) -> Result<MycDecryptedNip46Request, MycNip46WorkError> { 126 let expected_capability = self.operation.input().capability(); 127 if response.operation_id() != self.operation.operation_id() 128 || response.correlation_id() != self.operation.correlation_id() 129 || response.instance() != self.operation.instance() 130 || response.role() != MycProviderRole::Transport 131 || response.capability() != expected_capability 132 || response.nip44_version() != self.operation.input().nip44_version() 133 || !response.matches_operation(&self.operation) 134 { 135 return Err(work_error(MycNip46WorkErrorKind::InvalidBinding)); 136 } 137 let plaintext = response 138 .protected_payload() 139 .ok_or_else(|| work_error(MycNip46WorkErrorKind::DecryptionRejected))?; 140 let bounded = admit_myc_nip46_request(limits, plaintext) 141 .map_err(|_| work_error(MycNip46WorkErrorKind::DecryptionRejected))?; 142 let request = verify_myc_nip46_request(bounded) 143 .map_err(|_| work_error(MycNip46WorkErrorKind::DecryptionRejected))?; 144 let replay = bind_myc_nip46_replay(self.event, request) 145 .map_err(|_| work_error(MycNip46WorkErrorKind::DecryptionRejected))?; 146 Ok(MycDecryptedNip46Request { replay }) 147 } 148 } 149 150 impl fmt::Debug for MycNip46DecryptWork { 151 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 152 formatter 153 .debug_struct("MycNip46DecryptWork") 154 .field("event", &"[redacted]") 155 .field("operation", &self.operation) 156 .finish() 157 } 158 } 159 160 /// Constructs the exact transport-provider decryption work for one verified event. 161 pub fn prepare_myc_nip46_decrypt_work( 162 event: MycVerifiedNip46Event, 163 transport: &MycProviderBinding, 164 deadline: MycProviderDeadlineUnixMs, 165 ) -> Result<MycNip46DecryptWork, MycNip46WorkError> { 166 if transport.role() != MycProviderRole::Transport 167 || transport.expected_identity() != event.receiver_public_key() 168 { 169 return Err(work_error(MycNip46WorkErrorKind::InvalidBinding)); 170 } 171 let operation_id = MycProviderOperationId::from_bytes(derive_decrypt_identity( 172 DECRYPT_OPERATION_ID_DOMAIN, 173 event.event_id().as_bytes(), 174 )); 175 let correlation_id = MycProviderCorrelationId::from_bytes(derive_decrypt_identity( 176 DECRYPT_CORRELATION_ID_DOMAIN, 177 event.event_id().as_bytes(), 178 )); 179 let peer = MycProviderPublicIdentity::new(event.client_public_key().as_hex()) 180 .map_err(|_| work_error(MycNip46WorkErrorKind::InvalidBinding))?; 181 let input = match event.encryption_context() { 182 MycNip46EncryptionContext::Nip04 => { 183 MycProviderOperationInput::nip04_decrypt(peer, event.encrypted_content().as_bytes()) 184 } 185 MycNip46EncryptionContext::Nip44V2 => MycProviderOperationInput::nip44_decrypt( 186 peer, 187 MycProviderNip44Version::V2, 188 event.encrypted_content().as_bytes(), 189 ), 190 } 191 .map_err(|_| work_error(MycNip46WorkErrorKind::ProviderWorkRejected))?; 192 let operation = 193 MycProviderOperation::new(transport, operation_id, correlation_id, deadline, input) 194 .map_err(|_| work_error(MycNip46WorkErrorKind::ProviderWorkRejected))?; 195 Ok(MycNip46DecryptWork { event, operation }) 196 } 197 198 fn derive_decrypt_identity(domain: &[u8], event_id: &[u8; 32]) -> [u8; 32] { 199 let mut hasher = Sha256::new(); 200 hasher.update(domain); 201 hasher.update(event_id); 202 hasher.finalize().into() 203 } 204 205 /// Non-forgeable proof that the typed request came from the exact decrypt work. 206 pub struct MycDecryptedNip46Request { 207 replay: MycReplayBoundNip46Request, 208 } 209 210 impl fmt::Debug for MycDecryptedNip46Request { 211 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 212 formatter.write_str("MycDecryptedNip46Request([redacted])") 213 } 214 } 215 216 /// Exact request ready for durable admission but not provider execution. 217 pub struct MycPreparedNip46Request { 218 signer_request: MycSignerRequest, 219 request: Request, 220 encryption_context: MycNip46EncryptionContext, 221 } 222 223 impl MycPreparedNip46Request { 224 /// Returns the exact input for the short durable request-admission transaction. 225 #[must_use] 226 pub const fn signer_request(&self) -> &MycSignerRequest { 227 &self.signer_request 228 } 229 230 /// Returns the exact closed method admitted before persistence. 231 #[must_use] 232 pub const fn method(&self) -> MycSignerRequestMethod { 233 self.signer_request.method() 234 } 235 } 236 237 impl fmt::Debug for MycPreparedNip46Request { 238 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 239 formatter 240 .debug_struct("MycPreparedNip46Request") 241 .field("method", &self.method()) 242 .field("request", &"[redacted]") 243 .finish() 244 } 245 } 246 247 /// Converts one proven decrypted request into exact durable-admission input. 248 pub fn prepare_myc_nip46_request( 249 decrypted: MycDecryptedNip46Request, 250 nonce: MycSignerOperationNonce, 251 received_at: MycRequestReceivedAtUnixMs, 252 ) -> Result<MycPreparedNip46Request, MycNip46WorkError> { 253 let encryption_context = decrypted.replay.encryption_context(); 254 let method = MycSignerRequestMethod::parse(decrypted.replay.method()) 255 .ok_or_else(|| work_error(MycNip46WorkErrorKind::UnsupportedMethod))?; 256 let message: RequestMessage = serde_json::from_slice(decrypted.replay.canonical_request()) 257 .map_err(|_| work_error(MycNip46WorkErrorKind::InvalidBinding))?; 258 if message.payload().method().as_str() != method.as_str() 259 || message 260 .request_id() 261 .map(|request_id| request_id.as_str() != decrypted.replay.request_id().as_str()) 262 .unwrap_or(true) 263 { 264 return Err(work_error(MycNip46WorkErrorKind::InvalidBinding)); 265 } 266 let signer_request = MycSignerRequest::new( 267 decrypted.replay.client_public_key().clone(), 268 decrypted.replay.request_id().clone(), 269 decrypted.replay.event_id(), 270 method, 271 decrypted.replay.request_digest(), 272 nonce, 273 received_at, 274 ); 275 Ok(MycPreparedNip46Request { 276 signer_request, 277 request: message.request, 278 encryption_context, 279 }) 280 } 281 282 /// Exact Step 145 work classification. 283 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 284 pub enum MycNip46WorkKind { 285 Connect, 286 Local, 287 Provider, 288 } 289 290 enum Nip46WorkPayload { 291 Connect { 292 client: crate::MycNip46ClientPublicKey, 293 permissions: MycConnectionPermissionSet, 294 }, 295 Local, 296 Provider(MycProviderOperation), 297 } 298 299 /// Sealed request work constructed only after durable request admission. 300 pub struct MycNip46Work { 301 request: MycSignerRequestRecord, 302 connection: Option<MycConnectionRecord>, 303 method: MycSignerRequestMethod, 304 encryption_context: MycNip46EncryptionContext, 305 payload: Nip46WorkPayload, 306 } 307 308 impl MycNip46Work { 309 #[cfg(all(test, any(target_os = "linux", target_os = "macos")))] 310 pub(crate) fn local_for_test( 311 request: MycSignerRequestRecord, 312 connection: MycConnectionRecord, 313 method: MycSignerRequestMethod, 314 ) -> Self { 315 assert!(matches!( 316 method, 317 MycSignerRequestMethod::GetPublicKey 318 | MycSignerRequestMethod::GetSessionCapability 319 | MycSignerRequestMethod::Ping 320 | MycSignerRequestMethod::SwitchRelays 321 | MycSignerRequestMethod::Logout 322 )); 323 Self { 324 request, 325 connection: Some(connection), 326 method, 327 encryption_context: MycNip46EncryptionContext::Nip44V2, 328 payload: Nip46WorkPayload::Local, 329 } 330 } 331 332 /// Returns the exact admitted durable request record. 333 #[must_use] 334 pub const fn request_record(&self) -> &MycSignerRequestRecord { 335 &self.request 336 } 337 338 /// Returns the exact active connection used for non-connect work. 339 #[must_use] 340 pub const fn connection(&self) -> Option<&MycConnectionRecord> { 341 self.connection.as_ref() 342 } 343 344 /// Returns the exact admitted request method. 345 #[must_use] 346 pub const fn method(&self) -> MycSignerRequestMethod { 347 self.method 348 } 349 350 pub(crate) const fn encryption_context(&self) -> MycNip46EncryptionContext { 351 self.encryption_context 352 } 353 354 /// Returns the work class without exposing protected parameters. 355 #[must_use] 356 pub const fn kind(&self) -> MycNip46WorkKind { 357 match &self.payload { 358 Nip46WorkPayload::Connect { .. } => MycNip46WorkKind::Connect, 359 Nip46WorkPayload::Local => MycNip46WorkKind::Local, 360 Nip46WorkPayload::Provider(_) => MycNip46WorkKind::Provider, 361 } 362 } 363 364 /// Returns the provider operation only for provider-bearing work. 365 #[must_use] 366 pub const fn provider_operation(&self) -> Option<&MycProviderOperation> { 367 match &self.payload { 368 Nip46WorkPayload::Provider(operation) => Some(operation), 369 Nip46WorkPayload::Connect { .. } | Nip46WorkPayload::Local => None, 370 } 371 } 372 373 /// Constructs the exact Step 144 admission request for connect work. 374 pub fn connection_admission_request( 375 &self, 376 policy_generation: MycConnectionPolicyGeneration, 377 nonce: MycConnectionNonce, 378 observed_at: MycConnectionTimeUnixMs, 379 authorized_until: Option<MycConnectionTimeUnixMs>, 380 policy: MycConnectionAdmissionPolicy, 381 relay_id: MycRateRelayId, 382 ) -> Result<MycConnectionAdmissionRequest, MycNip46WorkError> { 383 let Nip46WorkPayload::Connect { 384 client, 385 permissions, 386 } = &self.payload 387 else { 388 return Err(work_error(MycNip46WorkErrorKind::InvalidBinding)); 389 }; 390 MycConnectionAdmissionRequest::new( 391 self.request.operation_id(), 392 client.clone(), 393 permissions.clone(), 394 policy_generation, 395 nonce, 396 observed_at, 397 authorized_until, 398 policy, 399 relay_id, 400 ) 401 .map_err(|_| work_error(MycNip46WorkErrorKind::InvalidBinding)) 402 } 403 } 404 405 impl fmt::Debug for MycNip46Work { 406 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 407 formatter 408 .debug_struct("MycNip46Work") 409 .field("method", &self.method) 410 .field("kind", &self.kind()) 411 .field("request", &"[redacted]") 412 .field( 413 "connection", 414 &self.connection.as_ref().map(|_| "[redacted]"), 415 ) 416 .finish() 417 } 418 } 419 420 /// Binds an admitted request and optional active connection to exact work. 421 /// 422 /// This function accepts no repository, host, transaction, provider executor, 423 /// clock, or entropy source. Provider execution therefore starts only after 424 /// the caller's durable request-admission transaction has ended. 425 pub fn prepare_myc_nip46_work( 426 prepared: MycPreparedNip46Request, 427 record: MycSignerRequestRecord, 428 connection: Option<MycConnectionRecord>, 429 providers: &MycProviderContract, 430 observed_at: MycConnectionTimeUnixMs, 431 provider_deadline: Option<MycProviderDeadlineUnixMs>, 432 ) -> Result<MycNip46Work, MycNip46WorkError> { 433 let MycPreparedNip46Request { 434 signer_request, 435 request, 436 encryption_context, 437 } = prepared; 438 if !record.matches_request(&signer_request) || observed_at.get() < record.received_at().get() { 439 return Err(work_error(MycNip46WorkErrorKind::InvalidBinding)); 440 } 441 let method = signer_request.method(); 442 let request = match request { 443 Request::Connect { 444 remote_signer_public_key, 445 secret, 446 requested_permissions, 447 client_metadata: _, 448 } => { 449 if connection.is_some() || provider_deadline.is_some() || secret.is_some() { 450 return Err(work_error(MycNip46WorkErrorKind::InvalidBinding)); 451 } 452 let transport = transport_binding(providers)?; 453 if remote_signer_public_key.to_hex() != transport.expected_identity().as_hex() { 454 return Err(work_error(MycNip46WorkErrorKind::InvalidBinding)); 455 } 456 let permissions = connect_permissions(requested_permissions.as_slice())?; 457 return Ok(MycNip46Work { 458 request: record, 459 connection: None, 460 method, 461 encryption_context, 462 payload: Nip46WorkPayload::Connect { 463 client: signer_request.client_public_key().clone(), 464 permissions, 465 }, 466 }); 467 } 468 Request::Custom { .. } => { 469 return Err(work_error(MycNip46WorkErrorKind::UnsupportedMethod)); 470 } 471 request => request, 472 }; 473 474 let connection = connection.ok_or_else(|| work_error(MycNip46WorkErrorKind::InvalidBinding))?; 475 if connection.client_public_key() != signer_request.client_public_key() { 476 return Err(work_error(MycNip46WorkErrorKind::InvalidBinding)); 477 } 478 if connection.status() != MycConnectionStatus::Active 479 || connection 480 .authorized_until() 481 .is_some_and(|until| observed_at > until) 482 { 483 return Err(work_error(MycNip46WorkErrorKind::ConnectionExpired)); 484 } 485 if let Some(permission) = required_permission(&request) { 486 let permission = protocol_permission(&permission.to_string())?; 487 if !connection.granted_permissions().contains(permission) { 488 return Err(work_error(MycNip46WorkErrorKind::PermissionDenied)); 489 } 490 } 491 492 let payload = match request { 493 Request::GetPublicKey 494 | Request::GetSessionCapability 495 | Request::Ping 496 | Request::SwitchRelays 497 | Request::Logout => { 498 if provider_deadline.is_some() { 499 return Err(work_error(MycNip46WorkErrorKind::InvalidBinding)); 500 } 501 Nip46WorkPayload::Local 502 } 503 Request::SignEvent(event) => { 504 let input = sign_event_input(&event, providers)?; 505 provider_work( 506 providers, 507 &record, 508 observed_at, 509 provider_deadline, 510 Ok(input), 511 )? 512 } 513 Request::Nip04Encrypt { 514 public_key, 515 plaintext, 516 } => provider_work( 517 providers, 518 &record, 519 observed_at, 520 provider_deadline, 521 MycProviderOperationInput::nip04_encrypt( 522 provider_identity(&public_key.to_hex())?, 523 plaintext.as_bytes(), 524 ), 525 )?, 526 Request::Nip04Decrypt { 527 public_key, 528 ciphertext, 529 } => provider_work( 530 providers, 531 &record, 532 observed_at, 533 provider_deadline, 534 MycProviderOperationInput::nip04_decrypt( 535 provider_identity(&public_key.to_hex())?, 536 ciphertext.as_bytes(), 537 ), 538 )?, 539 Request::Nip44Encrypt { 540 public_key, 541 plaintext, 542 } => provider_work( 543 providers, 544 &record, 545 observed_at, 546 provider_deadline, 547 MycProviderOperationInput::nip44_encrypt( 548 provider_identity(&public_key.to_hex())?, 549 MycProviderNip44Version::V2, 550 plaintext.as_bytes(), 551 ), 552 )?, 553 Request::Nip44Decrypt { 554 public_key, 555 ciphertext, 556 } => provider_work( 557 providers, 558 &record, 559 observed_at, 560 provider_deadline, 561 MycProviderOperationInput::nip44_decrypt( 562 provider_identity(&public_key.to_hex())?, 563 MycProviderNip44Version::V2, 564 ciphertext.as_bytes(), 565 ), 566 )?, 567 Request::Connect { .. } | Request::Custom { .. } => { 568 return Err(work_error(MycNip46WorkErrorKind::UnsupportedMethod)); 569 } 570 }; 571 572 Ok(MycNip46Work { 573 request: record, 574 connection: Some(connection), 575 method, 576 encryption_context, 577 payload, 578 }) 579 } 580 581 fn provider_work( 582 providers: &MycProviderContract, 583 record: &MycSignerRequestRecord, 584 observed_at: MycConnectionTimeUnixMs, 585 deadline: Option<MycProviderDeadlineUnixMs>, 586 input: Result<MycProviderOperationInput, crate::MycProviderContractError>, 587 ) -> Result<Nip46WorkPayload, MycNip46WorkError> { 588 let deadline = deadline.ok_or_else(|| work_error(MycNip46WorkErrorKind::InvalidBinding))?; 589 if deadline.get() <= observed_at.get() { 590 return Err(work_error(MycNip46WorkErrorKind::ProviderWorkRejected)); 591 } 592 let input = input.map_err(|_| work_error(MycNip46WorkErrorKind::ProviderWorkRejected))?; 593 let operation = MycProviderOperation::new( 594 user_binding(providers)?, 595 MycProviderOperationId::from_bytes(*record.operation_id().as_bytes()), 596 MycProviderCorrelationId::from_bytes(*record.correlation_id().as_bytes()), 597 deadline, 598 input, 599 ) 600 .map_err(|_| work_error(MycNip46WorkErrorKind::ProviderWorkRejected))?; 601 Ok(Nip46WorkPayload::Provider(operation)) 602 } 603 604 fn user_binding(providers: &MycProviderContract) -> Result<&MycProviderBinding, MycNip46WorkError> { 605 providers 606 .binding(MycProviderRole::User) 607 .ok_or_else(|| work_error(MycNip46WorkErrorKind::InvalidBinding)) 608 } 609 610 fn transport_binding( 611 providers: &MycProviderContract, 612 ) -> Result<&MycProviderBinding, MycNip46WorkError> { 613 providers 614 .binding(MycProviderRole::Transport) 615 .ok_or_else(|| work_error(MycNip46WorkErrorKind::InvalidBinding)) 616 } 617 618 fn sign_event_input( 619 event: &ConnectUnsignedEvent, 620 providers: &MycProviderContract, 621 ) -> Result<MycProviderOperationInput, MycNip46WorkError> { 622 let canonical = event.as_json(); 623 let unsigned: NostrUnsignedEvent = serde_json::from_str(&canonical) 624 .map_err(|_| work_error(MycNip46WorkErrorKind::ProviderWorkRejected))?; 625 let reencoded = serde_json::to_vec(&unsigned) 626 .map_err(|_| work_error(MycNip46WorkErrorKind::ProviderWorkRejected))?; 627 if reencoded.as_slice() != canonical.as_bytes() 628 || unsigned.verify_id().is_err() 629 || unsigned.pubkey.to_hex() != user_binding(providers)?.expected_identity().as_hex() 630 { 631 return Err(work_error(MycNip46WorkErrorKind::ProviderWorkRejected)); 632 } 633 MycProviderOperationInput::sign_event(canonical.as_bytes()) 634 .map_err(|_| work_error(MycNip46WorkErrorKind::ProviderWorkRejected)) 635 } 636 637 fn provider_identity(value: &str) -> Result<MycProviderPublicIdentity, MycNip46WorkError> { 638 MycProviderPublicIdentity::new(value) 639 .map_err(|_| work_error(MycNip46WorkErrorKind::ProviderWorkRejected)) 640 } 641 642 fn protocol_permission(value: &str) -> Result<MycConnectionPermission, MycNip46WorkError> { 643 MycConnectionPermission::parse(value) 644 .ok_or_else(|| work_error(MycNip46WorkErrorKind::UnsupportedMethod)) 645 } 646 647 fn connect_permissions( 648 permissions: &[radroots_nostr_connect::permission::Permission], 649 ) -> Result<MycConnectionPermissionSet, MycNip46WorkError> { 650 let permissions = permissions 651 .iter() 652 .map(connect_permission) 653 .collect::<Result<Vec<_>, _>>()?; 654 if permissions.iter().any(|permission| { 655 !matches!( 656 permission, 657 MycConnectionPermission::SignEvent(_) 658 | MycConnectionPermission::Nip04Encrypt 659 | MycConnectionPermission::Nip04Decrypt 660 | MycConnectionPermission::Nip44Encrypt 661 | MycConnectionPermission::Nip44Decrypt 662 | MycConnectionPermission::SwitchRelays 663 ) 664 }) { 665 return Err(work_error(MycNip46WorkErrorKind::PermissionDenied)); 666 } 667 MycConnectionPermissionSet::new(&permissions) 668 .map_err(|_| work_error(MycNip46WorkErrorKind::PermissionDenied)) 669 } 670 671 fn connect_permission( 672 permission: &radroots_nostr_connect::permission::Permission, 673 ) -> Result<MycConnectionPermission, MycNip46WorkError> { 674 if permission.method() == &radroots_nostr_connect::Method::SignEvent { 675 let parameter = permission 676 .parameter() 677 .ok_or_else(|| work_error(MycNip46WorkErrorKind::PermissionDenied))?; 678 let kind = parameter.strip_prefix("kind:").unwrap_or(parameter); 679 let parsed = kind 680 .parse::<u32>() 681 .ok() 682 .filter(|parsed| kind == parsed.to_string()) 683 .ok_or_else(|| work_error(MycNip46WorkErrorKind::PermissionDenied))?; 684 return Ok(MycConnectionPermission::SignEvent(parsed)); 685 } 686 if permission.parameter().is_some() { 687 return Err(work_error(MycNip46WorkErrorKind::PermissionDenied)); 688 } 689 protocol_permission(permission.method().as_str()) 690 } 691 692 #[cfg(test)] 693 mod tests { 694 use std::error::Error as _; 695 696 use nostr::{ 697 EventBuilder, JsonUtil as _, Keys, Kind, Tag, Timestamp, 698 UnsignedEvent as NostrUnsignedEvent, 699 nips::{nip04, nip44}, 700 }; 701 use radroots_nostr_connect::{ 702 Method, 703 message::{Request, RequestMessage, UnsignedEvent as ConnectUnsignedEvent}, 704 }; 705 706 use crate::{ 707 MycConfigDocumentV1, MycConfigProfile, MycNip46AuthoredTimePolicy, 708 MycNip46ObservedAtUnixSeconds, MycProviderCapability, MycProviderResponseObservedAtUnixMs, 709 admit_myc_nip46_event, parse_myc_config_v1, 710 provider_verification::verify_protected_response_for_test, verify_myc_nip46_event, 711 }; 712 713 use super::*; 714 715 const CONFIG: &str = include_str!("../contracts/services_hardening/config.v1.example.toml"); 716 const OBSERVED_AT_SECONDS: u64 = 1_725_000_000; 717 const RECEIVED_AT_MS: u64 = 1_725_000_000_000; 718 const PROVIDER_DEADLINE_MS: u64 = RECEIVED_AT_MS + 10_000; 719 720 fn keys(seed: u8) -> Keys { 721 Keys::parse(&format!("{seed:02x}{}", "00".repeat(31))).expect("test keys") 722 } 723 724 fn configuration() -> MycConfigDocumentV1 { 725 let source = CONFIG 726 .replacen( 727 "4444444444444444444444444444444444444444444444444444444444444444", 728 &keys(2).public_key().to_hex(), 729 1, 730 ) 731 .replacen( 732 "2222222222222222222222222222222222222222222222222222222222222222", 733 &keys(3).public_key().to_hex(), 734 1, 735 ); 736 parse_myc_config_v1(source.as_bytes(), MycConfigProfile::RepoLocal) 737 .expect("test configuration") 738 } 739 740 fn verified_event( 741 request: &Request, 742 nip44_v2: bool, 743 created_at: u64, 744 ) -> (MycVerifiedNip46Event, Vec<u8>) { 745 let client = keys(1); 746 let transport = keys(2); 747 let message = RequestMessage::try_new(format!("request-{created_at}"), request.clone()) 748 .expect("request message"); 749 let plaintext = serde_json::to_vec(&message).expect("canonical request"); 750 let content = if nip44_v2 { 751 nip44::encrypt( 752 client.secret_key(), 753 &transport.public_key(), 754 &plaintext, 755 nip44::Version::V2, 756 ) 757 .expect("NIP-44 encryption") 758 } else { 759 nip04::encrypt(client.secret_key(), &transport.public_key(), &plaintext) 760 .expect("NIP-04 encryption") 761 }; 762 let event = EventBuilder::new(Kind::Custom(24_133), content) 763 .tag(Tag::public_key(transport.public_key())) 764 .custom_created_at(Timestamp::from_secs(created_at)) 765 .sign_with_keys(&client) 766 .expect("signed request event"); 767 let config = configuration(); 768 let limits = MycNip46AdmissionLimits::from_config(&config).expect("admission limits"); 769 let wire = serde_json::to_vec(&event).expect("event wire"); 770 let event = verify_myc_nip46_event( 771 admit_myc_nip46_event(limits, &wire).expect("bounded event"), 772 config 773 .provider_contract() 774 .binding(MycProviderRole::Transport) 775 .expect("transport binding"), 776 MycNip46ObservedAtUnixSeconds::new(OBSERVED_AT_SECONDS).expect("observation"), 777 MycNip46AuthoredTimePolicy::new(120, 120).expect("authored-time policy"), 778 ) 779 .expect("verified event"); 780 (event, plaintext) 781 } 782 783 fn decrypted(request: &Request, nip44_v2: bool, created_at: u64) -> MycDecryptedNip46Request { 784 let (event, plaintext) = verified_event(request, nip44_v2, created_at); 785 let config = configuration(); 786 let transport = config 787 .provider_contract() 788 .binding(MycProviderRole::Transport) 789 .expect("transport binding"); 790 let work = prepare_myc_nip46_decrypt_work( 791 event, 792 transport, 793 MycProviderDeadlineUnixMs::new(PROVIDER_DEADLINE_MS).expect("deadline"), 794 ) 795 .expect("decrypt work"); 796 let response = verify_protected_response_for_test(transport, work.operation(), &plaintext) 797 .expect("verified provider response"); 798 work.complete( 799 response, 800 MycNip46AdmissionLimits::from_config(&config).expect("admission limits"), 801 ) 802 .expect("decrypted request") 803 } 804 805 fn prepared_request(request: Request, seed: u8) -> MycPreparedNip46Request { 806 prepare_myc_nip46_request( 807 decrypted(&request, true, OBSERVED_AT_SECONDS + u64::from(seed)), 808 MycSignerOperationNonce::from_injected_entropy([seed; 32]), 809 MycRequestReceivedAtUnixMs::new(RECEIVED_AT_MS + u64::from(seed)) 810 .expect("received time"), 811 ) 812 .expect("prepared request") 813 } 814 815 fn active_connection( 816 prepared: &MycPreparedNip46Request, 817 permissions: &[MycConnectionPermission], 818 authorized_until: Option<u64>, 819 ) -> MycConnectionRecord { 820 MycConnectionRecord::active_for_test( 821 prepared.signer_request.client_public_key().clone(), 822 MycConnectionPermissionSet::new(permissions).expect("permissions"), 823 authorized_until 824 .map(|value| MycConnectionTimeUnixMs::new(value).expect("authorization deadline")), 825 ) 826 } 827 828 fn unsigned_sign_event() -> ConnectUnsignedEvent { 829 unsigned_sign_event_for(3) 830 } 831 832 fn unsigned_sign_event_for(author_seed: u8) -> ConnectUnsignedEvent { 833 let event = NostrUnsignedEvent::new( 834 keys(author_seed).public_key(), 835 Timestamp::from_secs(OBSERVED_AT_SECONDS), 836 Kind::TextNote, 837 Vec::<Tag>::new(), 838 "bounded test event", 839 ); 840 ConnectUnsignedEvent::from_json(&event.as_json()).expect("connect unsigned event") 841 } 842 843 fn protocol_request(method: Method, params: Vec<String>) -> Request { 844 Request::from_parts(method, params).expect("protocol request") 845 } 846 847 #[test] 848 fn verified_decryption_is_bound_to_the_exact_event_and_context() { 849 let request = Request::Ping; 850 for (nip44_v2, expected_capability, expected_version) in [ 851 (false, MycProviderCapability::Nip04Decrypt, None), 852 ( 853 true, 854 MycProviderCapability::Nip44Decrypt, 855 Some(MycProviderNip44Version::V2), 856 ), 857 ] { 858 let (event, plaintext) = verified_event(&request, nip44_v2, OBSERVED_AT_SECONDS); 859 let config = configuration(); 860 let transport = config 861 .provider_contract() 862 .binding(MycProviderRole::Transport) 863 .expect("transport binding"); 864 let work = prepare_myc_nip46_decrypt_work( 865 event, 866 transport, 867 MycProviderDeadlineUnixMs::new(PROVIDER_DEADLINE_MS).expect("deadline"), 868 ) 869 .expect("decrypt work"); 870 assert_eq!(work.operation().role(), MycProviderRole::Transport); 871 assert_eq!(work.operation().input().capability(), expected_capability); 872 assert_eq!(work.operation().input().nip44_version(), expected_version); 873 let response = 874 verify_protected_response_for_test(transport, work.operation(), &plaintext) 875 .expect("verified response"); 876 work.complete( 877 response, 878 MycNip46AdmissionLimits::from_config(&config).expect("limits"), 879 ) 880 .expect("exact event completion"); 881 } 882 883 let (first_event, plaintext) = verified_event(&request, true, OBSERVED_AT_SECONDS + 1); 884 let (second_event, _) = verified_event(&request, true, OBSERVED_AT_SECONDS + 2); 885 let config = configuration(); 886 let transport = config 887 .provider_contract() 888 .binding(MycProviderRole::Transport) 889 .expect("transport binding"); 890 let first = prepare_myc_nip46_decrypt_work( 891 first_event, 892 transport, 893 MycProviderDeadlineUnixMs::new(PROVIDER_DEADLINE_MS).expect("deadline"), 894 ) 895 .expect("first work"); 896 let second = prepare_myc_nip46_decrypt_work( 897 second_event, 898 transport, 899 MycProviderDeadlineUnixMs::new(PROVIDER_DEADLINE_MS).expect("deadline"), 900 ) 901 .expect("second work"); 902 let response = verify_protected_response_for_test(transport, first.operation(), &plaintext) 903 .expect("first response"); 904 assert_ne!( 905 first.operation().operation_id(), 906 second.operation().operation_id() 907 ); 908 assert_eq!( 909 second 910 .complete( 911 response, 912 MycNip46AdmissionLimits::from_config(&config).expect("limits"), 913 ) 914 .expect_err("cross-event response"), 915 work_error(MycNip46WorkErrorKind::InvalidBinding) 916 ); 917 } 918 919 #[test] 920 fn exact_method_inventory_maps_to_local_and_user_provider_work() { 921 let peer = keys(4).public_key().to_hex(); 922 let cases = vec![ 923 (Request::GetPublicKey, None, MycNip46WorkKind::Local, None), 924 ( 925 Request::GetSessionCapability, 926 None, 927 MycNip46WorkKind::Local, 928 None, 929 ), 930 (Request::Ping, None, MycNip46WorkKind::Local, None), 931 (Request::Logout, None, MycNip46WorkKind::Local, None), 932 ( 933 Request::SwitchRelays, 934 Some(MycConnectionPermission::SwitchRelays), 935 MycNip46WorkKind::Local, 936 None, 937 ), 938 ( 939 Request::SignEvent(unsigned_sign_event()), 940 Some(MycConnectionPermission::SignEvent(1)), 941 MycNip46WorkKind::Provider, 942 Some(MycProviderCapability::SignEvent), 943 ), 944 ( 945 protocol_request(Method::Nip04Encrypt, vec![peer.clone(), "bounded".into()]), 946 Some(MycConnectionPermission::Nip04Encrypt), 947 MycNip46WorkKind::Provider, 948 Some(MycProviderCapability::Nip04Encrypt), 949 ), 950 ( 951 protocol_request( 952 Method::Nip04Decrypt, 953 vec![ 954 peer.clone(), 955 "QUFBQUFBQUFBQUFBQUFBQQ==?iv=QUFBQUFBQUFBQUFBQUFBQQ==".into(), 956 ], 957 ), 958 Some(MycConnectionPermission::Nip04Decrypt), 959 MycNip46WorkKind::Provider, 960 Some(MycProviderCapability::Nip04Decrypt), 961 ), 962 ( 963 protocol_request(Method::Nip44Encrypt, vec![peer.clone(), "bounded".into()]), 964 Some(MycConnectionPermission::Nip44Encrypt), 965 MycNip46WorkKind::Provider, 966 Some(MycProviderCapability::Nip44Encrypt), 967 ), 968 ( 969 protocol_request( 970 Method::Nip44Decrypt, 971 vec![ 972 peer, 973 "AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".into(), 974 ], 975 ), 976 Some(MycConnectionPermission::Nip44Decrypt), 977 MycNip46WorkKind::Provider, 978 Some(MycProviderCapability::Nip44Decrypt), 979 ), 980 ]; 981 for (index, (request, permission, expected_kind, expected_capability)) in 982 cases.into_iter().enumerate() 983 { 984 let prepared = prepared_request(request, u8::try_from(index + 1).expect("seed")); 985 let record = prepared.signer_request.admitted_record_for_test(); 986 let permissions = permission.into_iter().collect::<Vec<_>>(); 987 let connection = 988 active_connection(&prepared, &permissions, Some(RECEIVED_AT_MS + 100_000)); 989 let provider_deadline = (expected_kind == MycNip46WorkKind::Provider) 990 .then(|| MycProviderDeadlineUnixMs::new(PROVIDER_DEADLINE_MS).expect("deadline")); 991 let work = prepare_myc_nip46_work( 992 prepared, 993 record, 994 Some(connection), 995 configuration().provider_contract(), 996 MycConnectionTimeUnixMs::new(RECEIVED_AT_MS + 1_000).expect("observation"), 997 provider_deadline, 998 ) 999 .expect("governed work"); 1000 assert_eq!(work.kind(), expected_kind); 1001 assert_eq!( 1002 work.provider_operation() 1003 .map(|operation| operation.input().capability()), 1004 expected_capability 1005 ); 1006 if let Some(operation) = work.provider_operation() { 1007 assert_eq!(operation.role(), MycProviderRole::User); 1008 assert_eq!( 1009 operation.operation_id().as_bytes(), 1010 work.request_record().operation_id().as_bytes() 1011 ); 1012 } 1013 } 1014 } 1015 1016 #[test] 1017 fn connect_and_nonconnect_authorization_fail_closed() { 1018 let connect = protocol_request( 1019 Method::Connect, 1020 vec![ 1021 keys(2).public_key().to_hex(), 1022 String::new(), 1023 "nip44_encrypt,sign_event:1".into(), 1024 ], 1025 ); 1026 let prepared = prepared_request(connect, 20); 1027 let record = prepared.signer_request.admitted_record_for_test(); 1028 let work = prepare_myc_nip46_work( 1029 prepared, 1030 record, 1031 None, 1032 configuration().provider_contract(), 1033 MycConnectionTimeUnixMs::new(RECEIVED_AT_MS + 1_000).expect("observation"), 1034 None, 1035 ) 1036 .expect("connect work"); 1037 assert_eq!(work.kind(), MycNip46WorkKind::Connect); 1038 work.connection_admission_request( 1039 MycConnectionPolicyGeneration::new(1).expect("generation"), 1040 MycConnectionNonce::from_injected_entropy([0x44; 32]), 1041 MycConnectionTimeUnixMs::new(RECEIVED_AT_MS + 1_000).expect("observation"), 1042 None, 1043 MycConnectionAdmissionPolicy::ExplicitApproval, 1044 MycRateRelayId::new("primary").expect("relay"), 1045 ) 1046 .expect("admission request"); 1047 1048 for (request_seed, remote_signer_seed) in [(21, 3), (22, 4)] { 1049 let wrong_identity_connect = protocol_request( 1050 Method::Connect, 1051 vec![ 1052 keys(remote_signer_seed).public_key().to_hex(), 1053 String::new(), 1054 ], 1055 ); 1056 let prepared = prepared_request(wrong_identity_connect, request_seed); 1057 let record = prepared.signer_request.admitted_record_for_test(); 1058 assert_eq!( 1059 prepare_myc_nip46_work( 1060 prepared, 1061 record, 1062 None, 1063 configuration().provider_contract(), 1064 MycConnectionTimeUnixMs::new(RECEIVED_AT_MS + 1_000).expect("observation"), 1065 None, 1066 ) 1067 .expect_err("connect remote signer identity mismatch"), 1068 work_error(MycNip46WorkErrorKind::InvalidBinding) 1069 ); 1070 } 1071 1072 let secret_connect = protocol_request( 1073 Method::Connect, 1074 vec![keys(3).public_key().to_hex(), "forbidden-secret".into()], 1075 ); 1076 let prepared = prepared_request(secret_connect, 21); 1077 let record = prepared.signer_request.admitted_record_for_test(); 1078 assert_eq!( 1079 prepare_myc_nip46_work( 1080 prepared, 1081 record, 1082 None, 1083 configuration().provider_contract(), 1084 MycConnectionTimeUnixMs::new(RECEIVED_AT_MS + 1_000).expect("observation"), 1085 None, 1086 ) 1087 .expect_err("connect secret"), 1088 work_error(MycNip46WorkErrorKind::InvalidBinding) 1089 ); 1090 1091 let prepared = prepared_request( 1092 protocol_request( 1093 Method::Nip44Encrypt, 1094 vec![keys(4).public_key().to_hex(), "protected-marker".into()], 1095 ), 1096 22, 1097 ); 1098 let record = prepared.signer_request.admitted_record_for_test(); 1099 let denied = active_connection(&prepared, &[], Some(RECEIVED_AT_MS + 100_000)); 1100 assert_eq!( 1101 prepare_myc_nip46_work( 1102 prepared, 1103 record, 1104 Some(denied), 1105 configuration().provider_contract(), 1106 MycConnectionTimeUnixMs::new(RECEIVED_AT_MS + 1_000).expect("observation"), 1107 Some(MycProviderDeadlineUnixMs::new(PROVIDER_DEADLINE_MS).expect("deadline")), 1108 ) 1109 .expect_err("missing permission"), 1110 work_error(MycNip46WorkErrorKind::PermissionDenied) 1111 ); 1112 1113 let prepared = prepared_request(Request::Ping, 24); 1114 let record = prepared.signer_request.admitted_record_for_test(); 1115 let expired = active_connection(&prepared, &[], Some(RECEIVED_AT_MS + 500)); 1116 assert_eq!( 1117 prepare_myc_nip46_work( 1118 prepared, 1119 record, 1120 Some(expired), 1121 configuration().provider_contract(), 1122 MycConnectionTimeUnixMs::new(RECEIVED_AT_MS + 1_000).expect("observation"), 1123 None, 1124 ) 1125 .expect_err("expired connection"), 1126 work_error(MycNip46WorkErrorKind::ConnectionExpired) 1127 ); 1128 1129 let prepared = prepared_request( 1130 protocol_request( 1131 Method::Nip04Encrypt, 1132 vec![keys(4).public_key().to_hex(), "bounded".into()], 1133 ), 1134 25, 1135 ); 1136 let record = prepared.signer_request.admitted_record_for_test(); 1137 let connection = active_connection( 1138 &prepared, 1139 &[MycConnectionPermission::Nip04Encrypt], 1140 Some(RECEIVED_AT_MS + 100_000), 1141 ); 1142 assert_eq!( 1143 prepare_myc_nip46_work( 1144 prepared, 1145 record, 1146 Some(connection), 1147 configuration().provider_contract(), 1148 MycConnectionTimeUnixMs::new(PROVIDER_DEADLINE_MS).expect("observation"), 1149 Some(MycProviderDeadlineUnixMs::new(PROVIDER_DEADLINE_MS).expect("deadline")), 1150 ) 1151 .expect_err("nonfuture provider deadline"), 1152 work_error(MycNip46WorkErrorKind::ProviderWorkRejected) 1153 ); 1154 1155 let prepared = prepared_request(Request::SignEvent(unsigned_sign_event_for(4)), 28); 1156 let record = prepared.signer_request.admitted_record_for_test(); 1157 let connection = active_connection( 1158 &prepared, 1159 &[MycConnectionPermission::SignEvent(1)], 1160 Some(RECEIVED_AT_MS + 100_000), 1161 ); 1162 assert_eq!( 1163 prepare_myc_nip46_work( 1164 prepared, 1165 record, 1166 Some(connection), 1167 configuration().provider_contract(), 1168 MycConnectionTimeUnixMs::new(RECEIVED_AT_MS + 1_000).expect("observation"), 1169 Some(MycProviderDeadlineUnixMs::new(PROVIDER_DEADLINE_MS).expect("deadline")), 1170 ) 1171 .expect_err("wrong signing author"), 1172 work_error(MycNip46WorkErrorKind::ProviderWorkRejected) 1173 ); 1174 1175 let prepared = prepared_request(Request::Ping, 26); 1176 let mismatched = prepared_request(Request::Ping, 27) 1177 .signer_request 1178 .admitted_record_for_test(); 1179 let connection = active_connection(&prepared, &[], Some(RECEIVED_AT_MS + 100_000)); 1180 assert_eq!( 1181 prepare_myc_nip46_work( 1182 prepared, 1183 mismatched, 1184 Some(connection), 1185 configuration().provider_contract(), 1186 MycConnectionTimeUnixMs::new(RECEIVED_AT_MS + 1_000).expect("observation"), 1187 None, 1188 ) 1189 .expect_err("mismatched durable request"), 1190 work_error(MycNip46WorkErrorKind::InvalidBinding) 1191 ); 1192 1193 let custom = Request::Custom { 1194 method: Method::custom("vendor_method").expect("custom method"), 1195 params: vec!["protected-marker".to_owned()], 1196 }; 1197 assert_eq!( 1198 prepare_myc_nip46_request( 1199 decrypted(&custom, true, OBSERVED_AT_SECONDS + 23), 1200 MycSignerOperationNonce::from_injected_entropy([23; 32]), 1201 MycRequestReceivedAtUnixMs::new(RECEIVED_AT_MS).expect("received"), 1202 ) 1203 .expect_err("custom method"), 1204 work_error(MycNip46WorkErrorKind::UnsupportedMethod) 1205 ); 1206 } 1207 1208 #[test] 1209 fn errors_and_work_debug_are_source_free_and_redacted() { 1210 for kind in [ 1211 MycNip46WorkErrorKind::InvalidBinding, 1212 MycNip46WorkErrorKind::DecryptionRejected, 1213 MycNip46WorkErrorKind::UnsupportedMethod, 1214 MycNip46WorkErrorKind::PermissionDenied, 1215 MycNip46WorkErrorKind::ConnectionExpired, 1216 MycNip46WorkErrorKind::ProviderWorkRejected, 1217 ] { 1218 let error = work_error(kind); 1219 assert_eq!(error.kind(), kind); 1220 assert!(!error.code().is_empty()); 1221 assert!(!error.to_string().is_empty()); 1222 assert!(error.source().is_none()); 1223 } 1224 1225 let prepared = prepared_request(Request::Ping, 30); 1226 assert!(!format!("{prepared:?}").contains("request-")); 1227 assert!(!format!("{prepared:?}").contains("protected-marker")); 1228 assert!(MycProviderResponseObservedAtUnixMs::new(PROVIDER_DEADLINE_MS).is_ok()); 1229 } 1230 }