provider_contract.rs (47836B)
1 //! Typed, side-effect-free Myc signer-provider contract. 2 3 use core::fmt; 4 use std::error::Error; 5 use std::path::PathBuf; 6 7 use nostr::PublicKey; 8 use radroots_runtime_paths::ServiceCredentialArtifactName; 9 use serde_json::Value; 10 use sha2::{Digest, Sha256}; 11 use zeroize::Zeroizing; 12 13 /// Exact supported signer-provider contract version. 14 pub const MYC_PROVIDER_CONTRACT_VERSION: u32 = 1; 15 /// Maximum semantic provider input admitted before any wire encoding. 16 pub const MYC_PROVIDER_INPUT_MAX_BYTES: usize = 262_144; 17 /// Maximum untrusted provider output admitted before semantic verification. 18 pub const MYC_PROVIDER_OUTPUT_MAX_BYTES: usize = 1_048_576; 19 pub(crate) const MYC_PROVIDER_NIP44_PLAINTEXT_MAX_BYTES: usize = 65_536 - 128; 20 /// Maximum configured local-signer request deadline. 21 pub const MYC_PROVIDER_REQUEST_DEADLINE_MAX_MS: u64 = 30_000; 22 /// Maximum configured local-signer request body. 23 pub const MYC_PROVIDER_REQUEST_MAX_BYTES: u64 = 65_536; 24 /// Maximum configured local-signer response body. 25 pub const MYC_PROVIDER_RESPONSE_MAX_BYTES: u64 = 1_048_576; 26 /// Maximum configured local-signer concurrent operations. 27 pub const MYC_PROVIDER_CONCURRENCY_MAX: u32 = 64; 28 29 /// One explicit Myc identity role. 30 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] 31 pub enum MycProviderRole { 32 Transport, 33 User, 34 Discovery, 35 } 36 37 impl MycProviderRole { 38 /// Returns the exact contract spelling. 39 #[must_use] 40 pub const fn as_str(self) -> &'static str { 41 match self { 42 Self::Transport => "transport", 43 Self::User => "user", 44 Self::Discovery => "discovery", 45 } 46 } 47 48 const fn config_pointer(self) -> &'static str { 49 match self { 50 Self::Transport => "/identity/transport", 51 Self::User => "/identity/user", 52 Self::Discovery => "/identity/discovery/binding", 53 } 54 } 55 } 56 57 /// The only supported provider implementations. 58 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] 59 pub enum MycProviderKind { 60 EncryptedFile, 61 LocalSigner, 62 } 63 64 impl MycProviderKind { 65 /// Returns the exact configuration spelling. 66 #[must_use] 67 pub const fn as_str(self) -> &'static str { 68 match self { 69 Self::EncryptedFile => "encrypted_file", 70 Self::LocalSigner => "local_signer", 71 } 72 } 73 } 74 75 /// The complete closed signer-provider capability inventory. 76 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] 77 pub enum MycProviderCapability { 78 Describe, 79 PublicIdentity, 80 SignEvent, 81 Nip04Encrypt, 82 Nip04Decrypt, 83 Nip44Encrypt, 84 Nip44Decrypt, 85 } 86 87 impl MycProviderCapability { 88 pub(crate) const ALL: [Self; 7] = [ 89 Self::Describe, 90 Self::PublicIdentity, 91 Self::SignEvent, 92 Self::Nip04Encrypt, 93 Self::Nip04Decrypt, 94 Self::Nip44Encrypt, 95 Self::Nip44Decrypt, 96 ]; 97 98 /// Returns the exact protocol spelling. 99 #[must_use] 100 pub const fn as_str(self) -> &'static str { 101 match self { 102 Self::Describe => "describe", 103 Self::PublicIdentity => "public_identity", 104 Self::SignEvent => "sign_event", 105 Self::Nip04Encrypt => "nip04_encrypt", 106 Self::Nip04Decrypt => "nip04_decrypt", 107 Self::Nip44Encrypt => "nip44_encrypt", 108 Self::Nip44Decrypt => "nip44_decrypt", 109 } 110 } 111 112 const fn bit(self) -> u8 { 113 match self { 114 Self::Describe => 1 << 0, 115 Self::PublicIdentity => 1 << 1, 116 Self::SignEvent => 1 << 2, 117 Self::Nip04Encrypt => 1 << 3, 118 Self::Nip04Decrypt => 1 << 4, 119 Self::Nip44Encrypt => 1 << 5, 120 Self::Nip44Decrypt => 1 << 6, 121 } 122 } 123 } 124 125 /// One duplicate-free bounded capability set. 126 #[derive(Clone, Copy, PartialEq, Eq)] 127 pub struct MycProviderCapabilitySet(u8); 128 129 impl MycProviderCapabilitySet { 130 /// Constructs a set and rejects duplicate or empty inventories. 131 pub fn new(capabilities: &[MycProviderCapability]) -> Result<Self, MycProviderContractError> { 132 if capabilities.is_empty() || capabilities.len() > MycProviderCapability::ALL.len() { 133 return Err(contract_error( 134 MycProviderContractErrorKind::InvalidCapabilitySet, 135 )); 136 } 137 let mut bits = 0_u8; 138 for capability in capabilities { 139 let bit = capability.bit(); 140 if bits & bit != 0 { 141 return Err(contract_error( 142 MycProviderContractErrorKind::InvalidCapabilitySet, 143 )); 144 } 145 bits |= bit; 146 } 147 Ok(Self(bits)) 148 } 149 150 pub(crate) const fn for_role(role: MycProviderRole) -> Self { 151 let common = 152 MycProviderCapability::Describe.bit() | MycProviderCapability::PublicIdentity.bit(); 153 match role { 154 MycProviderRole::Transport => Self( 155 common 156 | MycProviderCapability::SignEvent.bit() 157 | MycProviderCapability::Nip04Encrypt.bit() 158 | MycProviderCapability::Nip04Decrypt.bit() 159 | MycProviderCapability::Nip44Encrypt.bit() 160 | MycProviderCapability::Nip44Decrypt.bit(), 161 ), 162 MycProviderRole::User => Self( 163 common 164 | MycProviderCapability::SignEvent.bit() 165 | MycProviderCapability::Nip04Encrypt.bit() 166 | MycProviderCapability::Nip04Decrypt.bit() 167 | MycProviderCapability::Nip44Encrypt.bit() 168 | MycProviderCapability::Nip44Decrypt.bit(), 169 ), 170 MycProviderRole::Discovery => Self(common | MycProviderCapability::SignEvent.bit()), 171 } 172 } 173 174 /// Returns whether this set contains the capability. 175 #[must_use] 176 pub const fn contains(self, capability: MycProviderCapability) -> bool { 177 self.0 & capability.bit() != 0 178 } 179 180 /// Returns the exact number of capabilities. 181 #[must_use] 182 pub const fn len(self) -> usize { 183 self.0.count_ones() as usize 184 } 185 186 /// Returns whether the set is empty. 187 #[must_use] 188 pub const fn is_empty(self) -> bool { 189 self.0 == 0 190 } 191 192 /// Iterates in the exact governed inventory order. 193 pub fn iter(self) -> impl Iterator<Item = MycProviderCapability> { 194 MycProviderCapability::ALL 195 .into_iter() 196 .filter(move |capability| self.contains(*capability)) 197 } 198 } 199 200 impl fmt::Debug for MycProviderCapabilitySet { 201 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 202 formatter.debug_list().entries(self.iter()).finish() 203 } 204 } 205 206 /// The role-assignment identity carried by every provider call. 207 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] 208 pub enum MycProviderInstanceId { 209 Transport, 210 User, 211 Discovery, 212 } 213 214 impl MycProviderInstanceId { 215 /// Returns the exact stable instance spelling. 216 #[must_use] 217 pub const fn as_str(self) -> &'static str { 218 match self { 219 Self::Transport => "transport", 220 Self::User => "user", 221 Self::Discovery => "discovery", 222 } 223 } 224 } 225 226 impl From<MycProviderRole> for MycProviderInstanceId { 227 fn from(role: MycProviderRole) -> Self { 228 match role { 229 MycProviderRole::Transport => Self::Transport, 230 MycProviderRole::User => Self::User, 231 MycProviderRole::Discovery => Self::Discovery, 232 } 233 } 234 } 235 236 /// One canonical expected or peer Nostr public identity. 237 #[derive(Clone, PartialEq, Eq, Hash)] 238 pub struct MycProviderPublicIdentity(Box<str>); 239 240 impl MycProviderPublicIdentity { 241 /// Validates exact lowercase 32-byte x-only public-key hex before allocation. 242 pub fn new(value: &str) -> Result<Self, MycProviderContractError> { 243 if value.len() != 64 244 || value 245 .bytes() 246 .any(|byte| !byte.is_ascii_hexdigit() || byte.is_ascii_uppercase()) 247 { 248 return Err(contract_error( 249 MycProviderContractErrorKind::InvalidIdentity, 250 )); 251 } 252 let public_key = PublicKey::from_hex(value) 253 .map_err(|_| contract_error(MycProviderContractErrorKind::InvalidIdentity))?; 254 public_key 255 .xonly() 256 .map_err(|_| contract_error(MycProviderContractErrorKind::InvalidIdentity))?; 257 if public_key.to_hex() != value { 258 return Err(contract_error( 259 MycProviderContractErrorKind::InvalidIdentity, 260 )); 261 } 262 Ok(Self(value.into())) 263 } 264 265 /// Returns the exact canonical public-key hex. 266 #[must_use] 267 pub fn as_hex(&self) -> &str { 268 &self.0 269 } 270 } 271 272 impl fmt::Debug for MycProviderPublicIdentity { 273 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 274 formatter.write_str("MycProviderPublicIdentity([redacted])") 275 } 276 } 277 278 /// One validated logical wrapping-credential reference. 279 #[derive(Clone, PartialEq, Eq, Hash)] 280 pub struct MycProviderCredentialReference(ServiceCredentialArtifactName); 281 282 impl MycProviderCredentialReference { 283 /// Validates the shared service credential-artifact vocabulary. 284 pub fn new(value: &str) -> Result<Self, MycProviderContractError> { 285 ServiceCredentialArtifactName::new(value) 286 .map(Self) 287 .map_err(|_| contract_error(MycProviderContractErrorKind::InvalidCredentialReference)) 288 } 289 290 /// Returns the exact logical reference, never credential material. 291 #[must_use] 292 pub fn as_str(&self) -> &str { 293 self.0.as_str() 294 } 295 } 296 297 impl fmt::Debug for MycProviderCredentialReference { 298 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 299 formatter.write_str("MycProviderCredentialReference([redacted])") 300 } 301 } 302 303 /// Validated local-signer transport limits. 304 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 305 pub struct MycLocalSignerLimits { 306 request_deadline_ms: u64, 307 request_max_bytes: u64, 308 response_max_bytes: u64, 309 concurrency: u32, 310 } 311 312 impl MycLocalSignerLimits { 313 /// Validates every configured transport resource limit. 314 pub fn new( 315 request_deadline_ms: u64, 316 request_max_bytes: u64, 317 response_max_bytes: u64, 318 concurrency: u32, 319 ) -> Result<Self, MycProviderContractError> { 320 if !(1..=MYC_PROVIDER_REQUEST_DEADLINE_MAX_MS).contains(&request_deadline_ms) 321 || !(1..=MYC_PROVIDER_REQUEST_MAX_BYTES).contains(&request_max_bytes) 322 || !(1..=MYC_PROVIDER_RESPONSE_MAX_BYTES).contains(&response_max_bytes) 323 || !(1..=MYC_PROVIDER_CONCURRENCY_MAX).contains(&concurrency) 324 { 325 return Err(contract_error(MycProviderContractErrorKind::InvalidLimits)); 326 } 327 Ok(Self { 328 request_deadline_ms, 329 request_max_bytes, 330 response_max_bytes, 331 concurrency, 332 }) 333 } 334 335 #[must_use] 336 pub const fn request_deadline_ms(self) -> u64 { 337 self.request_deadline_ms 338 } 339 340 #[must_use] 341 pub const fn request_max_bytes(self) -> u64 { 342 self.request_max_bytes 343 } 344 345 #[must_use] 346 pub const fn response_max_bytes(self) -> u64 { 347 self.response_max_bytes 348 } 349 350 #[must_use] 351 pub const fn concurrency(self) -> u32 { 352 self.concurrency 353 } 354 } 355 356 #[derive(Clone, PartialEq, Eq)] 357 enum ProviderLocation { 358 EncryptedFile { 359 envelope_path: PathBuf, 360 credential_reference: MycProviderCredentialReference, 361 }, 362 LocalSigner { 363 socket_path: PathBuf, 364 limits: MycLocalSignerLimits, 365 }, 366 } 367 368 /// One immutable provider assignment for one explicit identity role. 369 #[derive(Clone, PartialEq, Eq)] 370 pub struct MycProviderBinding { 371 role: MycProviderRole, 372 instance: MycProviderInstanceId, 373 kind: MycProviderKind, 374 expected_identity: MycProviderPublicIdentity, 375 required_capabilities: MycProviderCapabilitySet, 376 location: ProviderLocation, 377 } 378 379 impl MycProviderBinding { 380 #[must_use] 381 pub const fn role(&self) -> MycProviderRole { 382 self.role 383 } 384 385 #[must_use] 386 pub const fn instance(&self) -> MycProviderInstanceId { 387 self.instance 388 } 389 390 #[must_use] 391 pub const fn kind(&self) -> MycProviderKind { 392 self.kind 393 } 394 395 #[must_use] 396 pub const fn expected_identity(&self) -> &MycProviderPublicIdentity { 397 &self.expected_identity 398 } 399 400 #[must_use] 401 pub const fn required_capabilities(&self) -> MycProviderCapabilitySet { 402 self.required_capabilities 403 } 404 405 #[must_use] 406 pub const fn credential_reference(&self) -> Option<&MycProviderCredentialReference> { 407 match &self.location { 408 ProviderLocation::EncryptedFile { 409 credential_reference, 410 .. 411 } => Some(credential_reference), 412 ProviderLocation::LocalSigner { .. } => None, 413 } 414 } 415 416 #[must_use] 417 pub const fn local_signer_limits(&self) -> Option<MycLocalSignerLimits> { 418 match &self.location { 419 ProviderLocation::EncryptedFile { .. } => None, 420 ProviderLocation::LocalSigner { limits, .. } => Some(*limits), 421 } 422 } 423 424 pub(crate) fn encrypted_envelope_path(&self) -> Option<&std::path::Path> { 425 match &self.location { 426 ProviderLocation::EncryptedFile { envelope_path, .. } => Some(envelope_path), 427 ProviderLocation::LocalSigner { .. } => None, 428 } 429 } 430 431 #[cfg(any(target_os = "linux", target_os = "macos"))] 432 pub(crate) fn local_signer_socket_path(&self) -> Option<&std::path::Path> { 433 match &self.location { 434 ProviderLocation::LocalSigner { socket_path, .. } => Some(socket_path), 435 ProviderLocation::EncryptedFile { .. } => None, 436 } 437 } 438 439 fn location_is_valid(&self) -> bool { 440 match &self.location { 441 ProviderLocation::EncryptedFile { 442 envelope_path, 443 credential_reference, 444 } => envelope_path.is_absolute() && !credential_reference.as_str().is_empty(), 445 ProviderLocation::LocalSigner { 446 socket_path, 447 limits, 448 } => socket_path.is_absolute() && limits.concurrency() > 0, 449 } 450 } 451 } 452 453 impl fmt::Debug for MycProviderBinding { 454 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 455 formatter 456 .debug_struct("MycProviderBinding") 457 .field("role", &self.role) 458 .field("instance", &self.instance) 459 .field("kind", &self.kind) 460 .field("expected_identity", &"[redacted]") 461 .field("required_capabilities", &self.required_capabilities) 462 .field("location", &"[redacted]") 463 .field("location_valid", &self.location_is_valid()) 464 .finish() 465 } 466 } 467 468 /// The exact provider assignments derived from one admitted v1 configuration. 469 #[derive(PartialEq, Eq)] 470 pub struct MycProviderContract { 471 bindings: Box<[MycProviderBinding]>, 472 } 473 474 impl MycProviderContract { 475 pub(crate) fn from_normalized(document: &Value) -> Result<Self, MycProviderContractError> { 476 let mut bindings = Vec::with_capacity(3); 477 bindings.push(binding_from_config(document, MycProviderRole::Transport)?); 478 bindings.push(binding_from_config(document, MycProviderRole::User)?); 479 if json_bool(document, "/identity/discovery/enabled")? { 480 bindings.push(binding_from_config(document, MycProviderRole::Discovery)?); 481 } 482 if bindings.iter().enumerate().any(|(index, binding)| { 483 bindings[index + 1..] 484 .iter() 485 .any(|other| binding.expected_identity == other.expected_identity) 486 }) { 487 return Err(contract_error( 488 MycProviderContractErrorKind::InvalidIdentity, 489 )); 490 } 491 Ok(Self { 492 bindings: bindings.into_boxed_slice(), 493 }) 494 } 495 496 /// Returns all enabled bindings in transport, user, discovery order. 497 #[must_use] 498 pub fn bindings(&self) -> &[MycProviderBinding] { 499 &self.bindings 500 } 501 502 /// Returns the binding for one enabled role. 503 #[must_use] 504 pub fn binding(&self, role: MycProviderRole) -> Option<&MycProviderBinding> { 505 self.bindings.iter().find(|binding| binding.role == role) 506 } 507 } 508 509 impl fmt::Debug for MycProviderContract { 510 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 511 formatter 512 .debug_struct("MycProviderContract") 513 .field("version", &MYC_PROVIDER_CONTRACT_VERSION) 514 .field("binding_count", &self.bindings.len()) 515 .field( 516 "roles", 517 &self 518 .bindings 519 .iter() 520 .map(|binding| binding.role) 521 .collect::<Vec<_>>(), 522 ) 523 .finish() 524 } 525 } 526 527 fn binding_from_config( 528 document: &Value, 529 role: MycProviderRole, 530 ) -> Result<MycProviderBinding, MycProviderContractError> { 531 let prefix = role.config_pointer(); 532 let provider = json_string(document, &format!("{prefix}/provider"))?; 533 let expected_identity = MycProviderPublicIdentity::new(json_string( 534 document, 535 &format!("{prefix}/expected_public_key"), 536 )?)?; 537 let (kind, location) = match provider { 538 "encrypted_file" => { 539 let envelope_path = 540 PathBuf::from(json_string(document, &format!("{prefix}/envelope_path"))?); 541 if !envelope_path.is_absolute() { 542 return Err(contract_error( 543 MycProviderContractErrorKind::InvalidConfiguration, 544 )); 545 } 546 let credential_reference = MycProviderCredentialReference::new(json_string( 547 document, 548 &format!("{prefix}/credential_reference"), 549 )?)?; 550 ( 551 MycProviderKind::EncryptedFile, 552 ProviderLocation::EncryptedFile { 553 envelope_path, 554 credential_reference, 555 }, 556 ) 557 } 558 "local_signer" => { 559 let socket_path = 560 PathBuf::from(json_string(document, &format!("{prefix}/socket_path"))?); 561 if !socket_path.is_absolute() { 562 return Err(contract_error( 563 MycProviderContractErrorKind::InvalidConfiguration, 564 )); 565 } 566 let limits = MycLocalSignerLimits::new( 567 json_u64(document, &format!("{prefix}/request_deadline_ms"))?, 568 json_u64(document, &format!("{prefix}/request_max_bytes"))?, 569 json_u64(document, &format!("{prefix}/response_max_bytes"))?, 570 u32::try_from(json_u64(document, &format!("{prefix}/concurrency"))?) 571 .map_err(|_| contract_error(MycProviderContractErrorKind::InvalidLimits))?, 572 )?; 573 ( 574 MycProviderKind::LocalSigner, 575 ProviderLocation::LocalSigner { 576 socket_path, 577 limits, 578 }, 579 ) 580 } 581 _ => { 582 return Err(contract_error( 583 MycProviderContractErrorKind::InvalidConfiguration, 584 )); 585 } 586 }; 587 Ok(MycProviderBinding { 588 role, 589 instance: role.into(), 590 kind, 591 expected_identity, 592 required_capabilities: MycProviderCapabilitySet::for_role(role), 593 location, 594 }) 595 } 596 597 /// Positive absolute UTC millisecond deadline for one provider call. 598 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 599 pub struct MycProviderDeadlineUnixMs(u64); 600 601 impl MycProviderDeadlineUnixMs { 602 /// Validates a positive deadline representable by governed storage/time types. 603 pub fn new(value: u64) -> Result<Self, MycProviderContractError> { 604 if value == 0 || i64::try_from(value).is_err() { 605 return Err(contract_error( 606 MycProviderContractErrorKind::InvalidDeadline, 607 )); 608 } 609 Ok(Self(value)) 610 } 611 612 #[must_use] 613 pub const fn get(self) -> u64 { 614 self.0 615 } 616 } 617 618 macro_rules! provider_identity { 619 ($name:ident) => { 620 #[derive(Clone, Copy, PartialEq, Eq, Hash)] 621 pub struct $name([u8; 32]); 622 623 impl $name { 624 /// Wraps exact stable identity bytes. 625 #[must_use] 626 pub const fn from_bytes(bytes: [u8; 32]) -> Self { 627 Self(bytes) 628 } 629 630 /// Returns the exact identity bytes. 631 #[must_use] 632 pub const fn as_bytes(&self) -> &[u8; 32] { 633 &self.0 634 } 635 } 636 637 impl fmt::Debug for $name { 638 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 639 formatter.write_str(concat!(stringify!($name), "([redacted])")) 640 } 641 } 642 }; 643 } 644 645 provider_identity!(MycProviderOperationId); 646 provider_identity!(MycProviderCorrelationId); 647 648 /// Exact supported NIP-44 provider-operation version. 649 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] 650 pub enum MycProviderNip44Version { 651 V2, 652 } 653 654 impl MycProviderNip44Version { 655 #[must_use] 656 pub const fn as_u8(self) -> u8 { 657 match self { 658 Self::V2 => 2, 659 } 660 } 661 } 662 663 enum ProviderOperationInputKind { 664 Describe, 665 PublicIdentity, 666 SignEvent(Zeroizing<Vec<u8>>), 667 Nip04Encrypt { 668 peer: MycProviderPublicIdentity, 669 plaintext: Zeroizing<Vec<u8>>, 670 }, 671 Nip04Decrypt { 672 peer: MycProviderPublicIdentity, 673 ciphertext: Zeroizing<Vec<u8>>, 674 }, 675 Nip44Encrypt { 676 peer: MycProviderPublicIdentity, 677 version: MycProviderNip44Version, 678 plaintext: Zeroizing<Vec<u8>>, 679 }, 680 Nip44Decrypt { 681 peer: MycProviderPublicIdentity, 682 version: MycProviderNip44Version, 683 ciphertext: Zeroizing<Vec<u8>>, 684 }, 685 } 686 687 /// One bounded, non-forgeable semantic provider operation input. 688 pub struct MycProviderOperationInput { 689 kind: ProviderOperationInputKind, 690 } 691 692 impl MycProviderOperationInput { 693 #[must_use] 694 pub const fn describe() -> Self { 695 Self { 696 kind: ProviderOperationInputKind::Describe, 697 } 698 } 699 700 #[must_use] 701 pub const fn public_identity() -> Self { 702 Self { 703 kind: ProviderOperationInputKind::PublicIdentity, 704 } 705 } 706 707 pub fn sign_event(canonical_unsigned_event: &[u8]) -> Result<Self, MycProviderContractError> { 708 Ok(Self { 709 kind: ProviderOperationInputKind::SignEvent(copy_input( 710 canonical_unsigned_event, 711 false, 712 )?), 713 }) 714 } 715 716 pub fn nip04_encrypt( 717 peer: MycProviderPublicIdentity, 718 plaintext: &[u8], 719 ) -> Result<Self, MycProviderContractError> { 720 Ok(Self { 721 kind: ProviderOperationInputKind::Nip04Encrypt { 722 peer, 723 plaintext: copy_input(plaintext, true)?, 724 }, 725 }) 726 } 727 728 pub fn nip04_decrypt( 729 peer: MycProviderPublicIdentity, 730 ciphertext: &[u8], 731 ) -> Result<Self, MycProviderContractError> { 732 Ok(Self { 733 kind: ProviderOperationInputKind::Nip04Decrypt { 734 peer, 735 ciphertext: copy_input(ciphertext, false)?, 736 }, 737 }) 738 } 739 740 pub fn nip44_encrypt( 741 peer: MycProviderPublicIdentity, 742 version: MycProviderNip44Version, 743 plaintext: &[u8], 744 ) -> Result<Self, MycProviderContractError> { 745 if plaintext.len() > MYC_PROVIDER_NIP44_PLAINTEXT_MAX_BYTES { 746 return Err(contract_error(MycProviderContractErrorKind::InvalidInput)); 747 } 748 Ok(Self { 749 kind: ProviderOperationInputKind::Nip44Encrypt { 750 peer, 751 version, 752 plaintext: copy_input(plaintext, false)?, 753 }, 754 }) 755 } 756 757 pub fn nip44_decrypt( 758 peer: MycProviderPublicIdentity, 759 version: MycProviderNip44Version, 760 ciphertext: &[u8], 761 ) -> Result<Self, MycProviderContractError> { 762 Ok(Self { 763 kind: ProviderOperationInputKind::Nip44Decrypt { 764 peer, 765 version, 766 ciphertext: copy_input(ciphertext, false)?, 767 }, 768 }) 769 } 770 771 /// Returns the exact capability selected by this input. 772 #[must_use] 773 pub const fn capability(&self) -> MycProviderCapability { 774 match &self.kind { 775 ProviderOperationInputKind::Describe => MycProviderCapability::Describe, 776 ProviderOperationInputKind::PublicIdentity => MycProviderCapability::PublicIdentity, 777 ProviderOperationInputKind::SignEvent(_) => MycProviderCapability::SignEvent, 778 ProviderOperationInputKind::Nip04Encrypt { .. } => MycProviderCapability::Nip04Encrypt, 779 ProviderOperationInputKind::Nip04Decrypt { .. } => MycProviderCapability::Nip04Decrypt, 780 ProviderOperationInputKind::Nip44Encrypt { .. } => MycProviderCapability::Nip44Encrypt, 781 ProviderOperationInputKind::Nip44Decrypt { .. } => MycProviderCapability::Nip44Decrypt, 782 } 783 } 784 785 /// Returns the peer identity for peer-bound cryptographic operations. 786 #[must_use] 787 pub const fn peer(&self) -> Option<&MycProviderPublicIdentity> { 788 match &self.kind { 789 ProviderOperationInputKind::Nip04Encrypt { peer, .. } 790 | ProviderOperationInputKind::Nip04Decrypt { peer, .. } 791 | ProviderOperationInputKind::Nip44Encrypt { peer, .. } 792 | ProviderOperationInputKind::Nip44Decrypt { peer, .. } => Some(peer), 793 ProviderOperationInputKind::Describe 794 | ProviderOperationInputKind::PublicIdentity 795 | ProviderOperationInputKind::SignEvent(_) => None, 796 } 797 } 798 799 /// Returns the exact protected or event bytes, if any. 800 #[must_use] 801 pub fn bytes(&self) -> Option<&[u8]> { 802 match &self.kind { 803 ProviderOperationInputKind::SignEvent(bytes) => Some(bytes), 804 ProviderOperationInputKind::Nip04Encrypt { plaintext, .. } 805 | ProviderOperationInputKind::Nip44Encrypt { plaintext, .. } => Some(plaintext), 806 ProviderOperationInputKind::Nip04Decrypt { ciphertext, .. } 807 | ProviderOperationInputKind::Nip44Decrypt { ciphertext, .. } => Some(ciphertext), 808 ProviderOperationInputKind::Describe | ProviderOperationInputKind::PublicIdentity => { 809 None 810 } 811 } 812 } 813 814 fn owned(&self) -> Self { 815 let kind = match &self.kind { 816 ProviderOperationInputKind::Describe => ProviderOperationInputKind::Describe, 817 ProviderOperationInputKind::PublicIdentity => { 818 ProviderOperationInputKind::PublicIdentity 819 } 820 ProviderOperationInputKind::SignEvent(bytes) => { 821 ProviderOperationInputKind::SignEvent(bytes.clone()) 822 } 823 ProviderOperationInputKind::Nip04Encrypt { peer, plaintext } => { 824 ProviderOperationInputKind::Nip04Encrypt { 825 peer: peer.clone(), 826 plaintext: plaintext.clone(), 827 } 828 } 829 ProviderOperationInputKind::Nip04Decrypt { peer, ciphertext } => { 830 ProviderOperationInputKind::Nip04Decrypt { 831 peer: peer.clone(), 832 ciphertext: ciphertext.clone(), 833 } 834 } 835 ProviderOperationInputKind::Nip44Encrypt { 836 peer, 837 version, 838 plaintext, 839 } => ProviderOperationInputKind::Nip44Encrypt { 840 peer: peer.clone(), 841 version: *version, 842 plaintext: plaintext.clone(), 843 }, 844 ProviderOperationInputKind::Nip44Decrypt { 845 peer, 846 version, 847 ciphertext, 848 } => ProviderOperationInputKind::Nip44Decrypt { 849 peer: peer.clone(), 850 version: *version, 851 ciphertext: ciphertext.clone(), 852 }, 853 }; 854 Self { kind } 855 } 856 857 #[must_use] 858 pub const fn nip44_version(&self) -> Option<MycProviderNip44Version> { 859 match &self.kind { 860 ProviderOperationInputKind::Nip44Encrypt { version, .. } 861 | ProviderOperationInputKind::Nip44Decrypt { version, .. } => Some(*version), 862 _ => None, 863 } 864 } 865 } 866 867 impl fmt::Debug for MycProviderOperationInput { 868 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 869 formatter 870 .debug_struct("MycProviderOperationInput") 871 .field("capability", &self.capability()) 872 .field("peer", &self.peer().map(|_| "[redacted]")) 873 .field("payload", &self.bytes().map(|_| "[redacted]")) 874 .field("nip44_version", &self.nip44_version()) 875 .finish() 876 } 877 } 878 879 fn copy_input( 880 bytes: &[u8], 881 empty_allowed: bool, 882 ) -> Result<Zeroizing<Vec<u8>>, MycProviderContractError> { 883 if (!empty_allowed && bytes.is_empty()) || bytes.len() > MYC_PROVIDER_INPUT_MAX_BYTES { 884 return Err(contract_error(MycProviderContractErrorKind::InvalidInput)); 885 } 886 Ok(Zeroizing::new(bytes.to_vec())) 887 } 888 889 /// One fully bound provider operation before provider selection/execution. 890 pub struct MycProviderOperation { 891 role: MycProviderRole, 892 instance: MycProviderInstanceId, 893 provider: MycProviderKind, 894 operation_id: MycProviderOperationId, 895 correlation_id: MycProviderCorrelationId, 896 deadline: MycProviderDeadlineUnixMs, 897 expected_identity: MycProviderPublicIdentity, 898 input: MycProviderOperationInput, 899 } 900 901 impl MycProviderOperation { 902 /// Binds one operation to the exact configured provider assignment. 903 pub fn new( 904 binding: &MycProviderBinding, 905 operation_id: MycProviderOperationId, 906 correlation_id: MycProviderCorrelationId, 907 deadline: MycProviderDeadlineUnixMs, 908 input: MycProviderOperationInput, 909 ) -> Result<Self, MycProviderContractError> { 910 if !binding.required_capabilities.contains(input.capability()) { 911 return Err(contract_error( 912 MycProviderContractErrorKind::UnsupportedOperation, 913 )); 914 } 915 Ok(Self { 916 role: binding.role, 917 instance: binding.instance, 918 provider: binding.kind, 919 operation_id, 920 correlation_id, 921 deadline, 922 expected_identity: binding.expected_identity.clone(), 923 input, 924 }) 925 } 926 927 #[must_use] 928 pub const fn contract_version(&self) -> u32 { 929 MYC_PROVIDER_CONTRACT_VERSION 930 } 931 932 #[must_use] 933 pub const fn role(&self) -> MycProviderRole { 934 self.role 935 } 936 937 #[must_use] 938 pub const fn instance(&self) -> MycProviderInstanceId { 939 self.instance 940 } 941 942 #[must_use] 943 pub const fn provider(&self) -> MycProviderKind { 944 self.provider 945 } 946 947 #[must_use] 948 pub const fn operation_id(&self) -> MycProviderOperationId { 949 self.operation_id 950 } 951 952 #[must_use] 953 pub const fn correlation_id(&self) -> MycProviderCorrelationId { 954 self.correlation_id 955 } 956 957 #[must_use] 958 pub const fn deadline(&self) -> MycProviderDeadlineUnixMs { 959 self.deadline 960 } 961 962 #[must_use] 963 pub const fn expected_identity(&self) -> &MycProviderPublicIdentity { 964 &self.expected_identity 965 } 966 967 #[must_use] 968 pub const fn input(&self) -> &MycProviderOperationInput { 969 &self.input 970 } 971 972 pub(crate) fn owned_for_runtime(&self) -> Self { 973 Self { 974 role: self.role, 975 instance: self.instance, 976 provider: self.provider, 977 operation_id: self.operation_id, 978 correlation_id: self.correlation_id, 979 deadline: self.deadline, 980 expected_identity: self.expected_identity.clone(), 981 input: self.input.owned(), 982 } 983 } 984 985 pub(crate) fn binding_digest(&self) -> [u8; 32] { 986 let mut hasher = Sha256::new(); 987 hasher.update(b"radroots.myc.provider.operation_binding.v1\0"); 988 hash_framed(&mut hasher, self.role.as_str().as_bytes()); 989 hash_framed(&mut hasher, self.instance.as_str().as_bytes()); 990 hash_framed(&mut hasher, self.provider.as_str().as_bytes()); 991 hasher.update(self.operation_id.as_bytes()); 992 hasher.update(self.correlation_id.as_bytes()); 993 hasher.update(self.deadline.get().to_be_bytes()); 994 hash_framed(&mut hasher, self.expected_identity.as_hex().as_bytes()); 995 hash_framed(&mut hasher, self.input.capability().as_str().as_bytes()); 996 hash_framed( 997 &mut hasher, 998 self.input 999 .peer() 1000 .map(MycProviderPublicIdentity::as_hex) 1001 .unwrap_or_default() 1002 .as_bytes(), 1003 ); 1004 hasher.update( 1005 self.input 1006 .nip44_version() 1007 .map(MycProviderNip44Version::as_u8) 1008 .unwrap_or_default() 1009 .to_be_bytes(), 1010 ); 1011 hash_framed(&mut hasher, self.input.bytes().unwrap_or_default()); 1012 hasher.finalize().into() 1013 } 1014 } 1015 1016 fn hash_framed(hasher: &mut Sha256, value: &[u8]) { 1017 hasher.update(u64::try_from(value.len()).unwrap_or(u64::MAX).to_be_bytes()); 1018 hasher.update(value); 1019 } 1020 1021 impl fmt::Debug for MycProviderOperation { 1022 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 1023 formatter 1024 .debug_struct("MycProviderOperation") 1025 .field("contract_version", &MYC_PROVIDER_CONTRACT_VERSION) 1026 .field("role", &self.role) 1027 .field("instance", &self.instance) 1028 .field("provider", &self.provider) 1029 .field("operation_id", &"[redacted]") 1030 .field("correlation_id", &"[redacted]") 1031 .field("deadline", &"[redacted]") 1032 .field("expected_identity", &"[redacted]") 1033 .field("input", &self.input) 1034 .finish() 1035 } 1036 } 1037 1038 /// One bounded untrusted provider result body before independent verification. 1039 pub struct MycUntrustedProviderOutput(Zeroizing<Vec<u8>>); 1040 1041 impl MycUntrustedProviderOutput { 1042 /// Copies only after enforcing the hard response bound. 1043 pub fn new(bytes: &[u8]) -> Result<Self, MycProviderContractError> { 1044 if bytes.len() > MYC_PROVIDER_OUTPUT_MAX_BYTES { 1045 return Err(contract_error(MycProviderContractErrorKind::InvalidOutput)); 1046 } 1047 Ok(Self(Zeroizing::new(bytes.to_vec()))) 1048 } 1049 1050 /// Returns the untrusted bytes for the independent verifier only. 1051 #[must_use] 1052 pub fn as_bytes(&self) -> &[u8] { 1053 &self.0 1054 } 1055 } 1056 1057 impl fmt::Debug for MycUntrustedProviderOutput { 1058 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 1059 formatter.write_str("MycUntrustedProviderOutput([redacted])") 1060 } 1061 } 1062 1063 /// Stable source-free provider contract failure classification. 1064 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 1065 pub enum MycProviderContractErrorKind { 1066 InvalidConfiguration, 1067 InvalidIdentity, 1068 InvalidCredentialReference, 1069 InvalidLimits, 1070 InvalidCapabilitySet, 1071 InvalidDeadline, 1072 InvalidInput, 1073 InvalidOutput, 1074 UnsupportedOperation, 1075 } 1076 1077 impl MycProviderContractErrorKind { 1078 const fn message(self) -> &'static str { 1079 match self { 1080 Self::InvalidConfiguration => "provider configuration is invalid", 1081 Self::InvalidIdentity => "provider identity is invalid", 1082 Self::InvalidCredentialReference => "provider credential reference is invalid", 1083 Self::InvalidLimits => "provider resource limits are invalid", 1084 Self::InvalidCapabilitySet => "provider capability set is invalid", 1085 Self::InvalidDeadline => "provider deadline is invalid", 1086 Self::InvalidInput => "provider operation input is invalid", 1087 Self::InvalidOutput => "provider operation output is invalid", 1088 Self::UnsupportedOperation => "provider operation is unsupported for the role", 1089 } 1090 } 1091 } 1092 1093 /// One source-free provider contract failure. 1094 #[derive(Clone, Copy, PartialEq, Eq)] 1095 pub struct MycProviderContractError { 1096 kind: MycProviderContractErrorKind, 1097 } 1098 1099 impl MycProviderContractError { 1100 #[must_use] 1101 pub const fn kind(self) -> MycProviderContractErrorKind { 1102 self.kind 1103 } 1104 } 1105 1106 impl fmt::Debug for MycProviderContractError { 1107 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 1108 formatter 1109 .debug_struct("MycProviderContractError") 1110 .field("kind", &self.kind) 1111 .finish() 1112 } 1113 } 1114 1115 impl fmt::Display for MycProviderContractError { 1116 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 1117 formatter.write_str(self.kind.message()) 1118 } 1119 } 1120 1121 impl Error for MycProviderContractError {} 1122 1123 const fn contract_error(kind: MycProviderContractErrorKind) -> MycProviderContractError { 1124 MycProviderContractError { kind } 1125 } 1126 1127 fn json_string<'a>( 1128 document: &'a Value, 1129 pointer: &str, 1130 ) -> Result<&'a str, MycProviderContractError> { 1131 document 1132 .pointer(pointer) 1133 .and_then(Value::as_str) 1134 .ok_or_else(|| contract_error(MycProviderContractErrorKind::InvalidConfiguration)) 1135 } 1136 1137 fn json_u64(document: &Value, pointer: &str) -> Result<u64, MycProviderContractError> { 1138 document 1139 .pointer(pointer) 1140 .and_then(Value::as_u64) 1141 .ok_or_else(|| contract_error(MycProviderContractErrorKind::InvalidConfiguration)) 1142 } 1143 1144 fn json_bool(document: &Value, pointer: &str) -> Result<bool, MycProviderContractError> { 1145 document 1146 .pointer(pointer) 1147 .and_then(Value::as_bool) 1148 .ok_or_else(|| contract_error(MycProviderContractErrorKind::InvalidConfiguration)) 1149 } 1150 1151 #[cfg(test)] 1152 mod tests { 1153 use crate::{MycConfigProfile, parse_myc_config_v1}; 1154 1155 use super::*; 1156 1157 const CONFIG: &[u8] = include_bytes!("../contracts/services_hardening/config.v1.example.toml"); 1158 1159 #[test] 1160 fn capability_sets_are_closed_ordered_and_duplicate_free() { 1161 let transport = MycProviderCapabilitySet::for_role(MycProviderRole::Transport); 1162 assert_eq!(transport.len(), 7); 1163 assert!(transport.contains(MycProviderCapability::SignEvent)); 1164 assert_eq!( 1165 MycProviderCapabilitySet::for_role(MycProviderRole::User) 1166 .iter() 1167 .collect::<Vec<_>>(), 1168 MycProviderCapability::ALL 1169 ); 1170 assert_eq!( 1171 MycProviderCapabilitySet::for_role(MycProviderRole::Discovery) 1172 .iter() 1173 .collect::<Vec<_>>(), 1174 vec![ 1175 MycProviderCapability::Describe, 1176 MycProviderCapability::PublicIdentity, 1177 MycProviderCapability::SignEvent, 1178 ] 1179 ); 1180 assert_eq!( 1181 MycProviderCapabilitySet::new(&[ 1182 MycProviderCapability::Describe, 1183 MycProviderCapability::Describe, 1184 ]) 1185 .unwrap_err() 1186 .kind(), 1187 MycProviderContractErrorKind::InvalidCapabilitySet 1188 ); 1189 assert!(MycProviderCapabilitySet::new(&[]).is_err()); 1190 } 1191 1192 #[test] 1193 fn identities_references_limits_and_deadlines_are_exactly_bounded() { 1194 let identity = "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"; 1195 assert!(MycProviderPublicIdentity::new(identity).is_ok()); 1196 assert!(MycProviderPublicIdentity::new(&identity.to_uppercase()).is_err()); 1197 assert!(MycProviderPublicIdentity::new(&"1".repeat(63)).is_err()); 1198 1199 assert!(MycProviderCredentialReference::new("a").is_ok()); 1200 assert!(MycProviderCredentialReference::new(&"a".repeat(128)).is_ok()); 1201 assert!(MycProviderCredentialReference::new(&"a".repeat(129)).is_err()); 1202 assert!(MycProviderCredentialReference::new("../credential").is_err()); 1203 1204 assert!(MycLocalSignerLimits::new(1, 1, 1, 1).is_ok()); 1205 assert!( 1206 MycLocalSignerLimits::new( 1207 MYC_PROVIDER_REQUEST_DEADLINE_MAX_MS, 1208 MYC_PROVIDER_REQUEST_MAX_BYTES, 1209 MYC_PROVIDER_RESPONSE_MAX_BYTES, 1210 MYC_PROVIDER_CONCURRENCY_MAX, 1211 ) 1212 .is_ok() 1213 ); 1214 for result in [ 1215 MycLocalSignerLimits::new(0, 1, 1, 1), 1216 MycLocalSignerLimits::new(1, 0, 1, 1), 1217 MycLocalSignerLimits::new(1, 1, 0, 1), 1218 MycLocalSignerLimits::new(1, 1, 1, 0), 1219 MycLocalSignerLimits::new(MYC_PROVIDER_REQUEST_DEADLINE_MAX_MS + 1, 1, 1, 1), 1220 MycLocalSignerLimits::new(1, MYC_PROVIDER_REQUEST_MAX_BYTES + 1, 1, 1), 1221 MycLocalSignerLimits::new(1, 1, MYC_PROVIDER_RESPONSE_MAX_BYTES + 1, 1), 1222 MycLocalSignerLimits::new(1, 1, 1, MYC_PROVIDER_CONCURRENCY_MAX + 1), 1223 ] { 1224 assert_eq!( 1225 result.unwrap_err().kind(), 1226 MycProviderContractErrorKind::InvalidLimits 1227 ); 1228 } 1229 1230 assert!(MycProviderDeadlineUnixMs::new(1).is_ok()); 1231 assert!(MycProviderDeadlineUnixMs::new(i64::MAX as u64).is_ok()); 1232 assert!(MycProviderDeadlineUnixMs::new(0).is_err()); 1233 assert!(MycProviderDeadlineUnixMs::new(i64::MAX as u64 + 1).is_err()); 1234 } 1235 1236 #[test] 1237 fn operation_inputs_are_bounded_before_copy_and_redacted() { 1238 let peer = MycProviderPublicIdentity::new(&"2".repeat(64)).expect("peer"); 1239 let exact = vec![b'x'; MYC_PROVIDER_INPUT_MAX_BYTES]; 1240 assert!(MycProviderOperationInput::sign_event(&exact).is_ok()); 1241 assert!(MycProviderOperationInput::sign_event(&[]).is_err()); 1242 assert!( 1243 MycProviderOperationInput::sign_event(&vec![b'x'; MYC_PROVIDER_INPUT_MAX_BYTES + 1]) 1244 .is_err() 1245 ); 1246 let encrypt = MycProviderOperationInput::nip44_encrypt( 1247 peer, 1248 MycProviderNip44Version::V2, 1249 b"protected-value", 1250 ) 1251 .expect("operation"); 1252 let rendered = format!("{encrypt:?}"); 1253 assert!(!rendered.contains("protected-value")); 1254 assert_eq!(encrypt.capability(), MycProviderCapability::Nip44Encrypt); 1255 assert_eq!( 1256 encrypt.nip44_version().map(MycProviderNip44Version::as_u8), 1257 Some(2) 1258 ); 1259 assert!( 1260 MycProviderOperationInput::nip44_encrypt( 1261 MycProviderPublicIdentity::new(&"2".repeat(64)).expect("peer"), 1262 MycProviderNip44Version::V2, 1263 &vec![0; MYC_PROVIDER_NIP44_PLAINTEXT_MAX_BYTES] 1264 ) 1265 .is_ok() 1266 ); 1267 assert!( 1268 MycProviderOperationInput::nip44_encrypt( 1269 MycProviderPublicIdentity::new(&"2".repeat(64)).expect("peer"), 1270 MycProviderNip44Version::V2, 1271 &vec![0; MYC_PROVIDER_NIP44_PLAINTEXT_MAX_BYTES + 1] 1272 ) 1273 .is_err() 1274 ); 1275 1276 assert!(MycUntrustedProviderOutput::new(&vec![0; MYC_PROVIDER_OUTPUT_MAX_BYTES]).is_ok()); 1277 assert!( 1278 MycUntrustedProviderOutput::new(&vec![0; MYC_PROVIDER_OUTPUT_MAX_BYTES + 1]).is_err() 1279 ); 1280 } 1281 1282 #[test] 1283 fn operation_binding_covers_every_independently_variable_request_field() { 1284 let config = parse_myc_config_v1(CONFIG, MycConfigProfile::RepoLocal).expect("config"); 1285 let user = config 1286 .provider_contract() 1287 .binding(MycProviderRole::User) 1288 .expect("user binding"); 1289 let transport = config 1290 .provider_contract() 1291 .binding(MycProviderRole::Transport) 1292 .expect("transport binding"); 1293 let operation_id = MycProviderOperationId::from_bytes([0x11; 32]); 1294 let correlation_id = MycProviderCorrelationId::from_bytes([0x22; 32]); 1295 let deadline = MycProviderDeadlineUnixMs::new(1_900_000_000_000).expect("deadline"); 1296 let operation = 1297 |binding: &MycProviderBinding, operation_id, correlation_id, deadline, input| { 1298 MycProviderOperation::new(binding, operation_id, correlation_id, deadline, input) 1299 .expect("operation") 1300 }; 1301 let base = operation( 1302 user, 1303 operation_id, 1304 correlation_id, 1305 deadline, 1306 MycProviderOperationInput::public_identity(), 1307 ); 1308 for changed in [ 1309 operation( 1310 user, 1311 MycProviderOperationId::from_bytes([0x12; 32]), 1312 correlation_id, 1313 deadline, 1314 MycProviderOperationInput::public_identity(), 1315 ), 1316 operation( 1317 user, 1318 operation_id, 1319 MycProviderCorrelationId::from_bytes([0x23; 32]), 1320 deadline, 1321 MycProviderOperationInput::public_identity(), 1322 ), 1323 operation( 1324 user, 1325 operation_id, 1326 correlation_id, 1327 MycProviderDeadlineUnixMs::new(deadline.get() + 1).expect("changed deadline"), 1328 MycProviderOperationInput::public_identity(), 1329 ), 1330 operation( 1331 user, 1332 operation_id, 1333 correlation_id, 1334 deadline, 1335 MycProviderOperationInput::sign_event(b"{}").expect("sign event"), 1336 ), 1337 operation( 1338 transport, 1339 operation_id, 1340 correlation_id, 1341 deadline, 1342 MycProviderOperationInput::public_identity(), 1343 ), 1344 ] { 1345 assert_ne!(base.binding_digest(), changed.binding_digest()); 1346 } 1347 1348 let first_peer = MycProviderPublicIdentity::new(&"4".repeat(64)).expect("first peer"); 1349 let second_peer = MycProviderPublicIdentity::new( 1350 "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", 1351 ) 1352 .expect("second peer"); 1353 let first = operation( 1354 user, 1355 operation_id, 1356 correlation_id, 1357 deadline, 1358 MycProviderOperationInput::nip04_encrypt(first_peer.clone(), b"first") 1359 .expect("first input"), 1360 ); 1361 let changed_bytes = operation( 1362 user, 1363 operation_id, 1364 correlation_id, 1365 deadline, 1366 MycProviderOperationInput::nip04_encrypt(first_peer, b"second").expect("changed bytes"), 1367 ); 1368 let changed_peer = operation( 1369 user, 1370 operation_id, 1371 correlation_id, 1372 deadline, 1373 MycProviderOperationInput::nip04_encrypt(second_peer, b"first").expect("changed peer"), 1374 ); 1375 assert_ne!(first.binding_digest(), changed_bytes.binding_digest()); 1376 assert_ne!(first.binding_digest(), changed_peer.binding_digest()); 1377 } 1378 1379 #[test] 1380 fn every_public_error_is_fixed_and_source_free() { 1381 for kind in [ 1382 MycProviderContractErrorKind::InvalidConfiguration, 1383 MycProviderContractErrorKind::InvalidIdentity, 1384 MycProviderContractErrorKind::InvalidCredentialReference, 1385 MycProviderContractErrorKind::InvalidLimits, 1386 MycProviderContractErrorKind::InvalidCapabilitySet, 1387 MycProviderContractErrorKind::InvalidDeadline, 1388 MycProviderContractErrorKind::InvalidInput, 1389 MycProviderContractErrorKind::InvalidOutput, 1390 MycProviderContractErrorKind::UnsupportedOperation, 1391 ] { 1392 let error = contract_error(kind); 1393 assert_eq!(error.kind(), kind); 1394 assert!(!error.to_string().is_empty()); 1395 assert!(error.source().is_none()); 1396 assert!(!format!("{error:?}").contains('/')); 1397 } 1398 } 1399 }