myc

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

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 }