myc

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

provider_local_signer.rs (41014B)


      1 //! Strict bounded local-signer transport over the hardened Lib admin client.
      2 
      3 use core::fmt;
      4 #[cfg(any(target_os = "linux", target_os = "macos"))]
      5 use core::time::Duration;
      6 use std::error::Error;
      7 
      8 use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
      9 use zeroize::Zeroizing;
     10 
     11 #[cfg(all(test, not(any(target_os = "linux", target_os = "macos"))))]
     12 use crate::MycProviderNip44Version;
     13 use crate::{
     14     MYC_PROVIDER_OUTPUT_MAX_BYTES, MycProviderBinding, MycProviderCapability,
     15     MycProviderInstanceId, MycProviderOperation, MycProviderRole,
     16 };
     17 #[cfg(any(target_os = "linux", target_os = "macos"))]
     18 use crate::{MycProviderKind, MycProviderNip44Version, MycProviderPublicIdentity};
     19 
     20 /// Exact local-signer wire contract version.
     21 pub const MYC_LOCAL_SIGNER_TRANSPORT_CONTRACT_VERSION: u32 = 1;
     22 /// The one fixed local-signer provider endpoint.
     23 pub const MYC_LOCAL_SIGNER_ENDPOINT: &str = "/v1/provider/operation";
     24 
     25 const PROTECTED_WIRE_HEX_MAX_UTF8_BYTES: usize = MYC_PROVIDER_OUTPUT_MAX_BYTES * 2;
     26 
     27 /// Stable source-free local-signer transport failure classification.
     28 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
     29 pub enum MycLocalSignerTransportErrorKind {
     30     InvalidBinding,
     31     InvalidOperation,
     32     InvalidLimits,
     33     RequestEncoding,
     34     RequestLimit,
     35     Transport,
     36     Deadline,
     37     Response,
     38     RemoteFailure,
     39     UnsupportedPlatform,
     40 }
     41 
     42 impl MycLocalSignerTransportErrorKind {
     43     /// Returns the stable machine-facing safe code.
     44     #[must_use]
     45     pub const fn code(self) -> &'static str {
     46         match self {
     47             Self::InvalidBinding => "local_signer_binding_invalid",
     48             Self::InvalidOperation => "local_signer_operation_invalid",
     49             Self::InvalidLimits => "local_signer_limits_invalid",
     50             Self::RequestEncoding => "local_signer_request_encoding_failed",
     51             Self::RequestLimit => "local_signer_request_limit_exceeded",
     52             Self::Transport => "local_signer_transport_failed",
     53             Self::Deadline => "local_signer_deadline_exceeded",
     54             Self::Response => "local_signer_response_invalid",
     55             Self::RemoteFailure => "local_signer_remote_failed",
     56             Self::UnsupportedPlatform => "local_signer_platform_unsupported",
     57         }
     58     }
     59 
     60     const fn message(self) -> &'static str {
     61         match self {
     62             Self::InvalidBinding => "local signer binding is invalid",
     63             Self::InvalidOperation => "local signer operation is invalid",
     64             Self::InvalidLimits => "local signer limits are invalid",
     65             Self::RequestEncoding => "local signer request encoding failed",
     66             Self::RequestLimit => "local signer request exceeds its bound",
     67             Self::Transport => "local signer transport failed",
     68             Self::Deadline => "local signer deadline elapsed",
     69             Self::Response => "local signer response is invalid",
     70             Self::RemoteFailure => "local signer returned a failure",
     71             Self::UnsupportedPlatform => "local signer transport is unsupported",
     72         }
     73     }
     74 }
     75 
     76 /// One source-free local-signer transport failure.
     77 #[derive(Clone, Copy, PartialEq, Eq)]
     78 pub struct MycLocalSignerTransportError {
     79     kind: MycLocalSignerTransportErrorKind,
     80 }
     81 
     82 impl MycLocalSignerTransportError {
     83     /// Returns the stable failure kind.
     84     #[must_use]
     85     pub const fn kind(self) -> MycLocalSignerTransportErrorKind {
     86         self.kind
     87     }
     88 
     89     /// Returns the stable machine-facing safe code.
     90     #[must_use]
     91     pub const fn code(self) -> &'static str {
     92         self.kind.code()
     93     }
     94 }
     95 
     96 impl fmt::Debug for MycLocalSignerTransportError {
     97     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     98         formatter
     99             .debug_struct("MycLocalSignerTransportError")
    100             .field("kind", &self.kind)
    101             .finish()
    102     }
    103 }
    104 
    105 impl fmt::Display for MycLocalSignerTransportError {
    106     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    107         formatter.write_str(self.kind.message())
    108     }
    109 }
    110 
    111 impl Error for MycLocalSignerTransportError {}
    112 
    113 const fn transport_error(kind: MycLocalSignerTransportErrorKind) -> MycLocalSignerTransportError {
    114     MycLocalSignerTransportError { kind }
    115 }
    116 
    117 /// A strictly decoded but semantically untrusted local-signer success.
    118 ///
    119 /// Step 135 independently verifies every binding and operation result before
    120 /// any caller may treat this value as usable. Cancellation or timeout does not
    121 /// prove that the external signer performed no operation, and this value never
    122 /// represents publication.
    123 pub struct MycLocalSignerUntrustedResponse {
    124     outer_correlation_id: Box<str>,
    125     response: LocalSignerResponse,
    126 }
    127 
    128 impl MycLocalSignerUntrustedResponse {
    129     #[cfg(test)]
    130     pub(crate) fn from_parts(parts: LocalSignerUntrustedParts) -> Self {
    131         Self {
    132             outer_correlation_id: parts.outer_correlation_id,
    133             response: parts.response,
    134         }
    135     }
    136 
    137     pub(crate) fn into_parts(self) -> LocalSignerUntrustedParts {
    138         LocalSignerUntrustedParts {
    139             outer_correlation_id: self.outer_correlation_id,
    140             response: self.response,
    141         }
    142     }
    143 }
    144 
    145 pub(crate) struct LocalSignerUntrustedParts {
    146     pub(crate) outer_correlation_id: Box<str>,
    147     pub(crate) response: LocalSignerResponse,
    148 }
    149 
    150 impl fmt::Debug for MycLocalSignerUntrustedResponse {
    151     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    152         formatter
    153             .debug_struct("MycLocalSignerUntrustedResponse")
    154             .field(
    155                 "correlation_id",
    156                 &if self.outer_correlation_id.is_empty() {
    157                     "[invalid]"
    158                 } else {
    159                     "[redacted]"
    160                 },
    161             )
    162             .field("provider_instance", &self.response.provider_instance)
    163             .field("role", &self.response.role)
    164             .field("capability", &self.response.capability)
    165             .field("result", &"[redacted]")
    166             .finish()
    167     }
    168 }
    169 
    170 #[cfg(any(target_os = "linux", target_os = "macos"))]
    171 /// Bounded client for one configured local-signer Unix socket.
    172 pub struct MycLocalSignerClient {
    173     transport: radroots_service_host::AdminClient,
    174     target: radroots_service_host::AdminClientTarget,
    175     permits: tokio::sync::Semaphore,
    176     request_deadline: Duration,
    177     role: MycProviderRole,
    178     instance: MycProviderInstanceId,
    179     expected_identity: MycProviderPublicIdentity,
    180 }
    181 
    182 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
    183 /// Unsupported-target local-signer client placeholder.
    184 pub struct MycLocalSignerClient {
    185     _private: (),
    186 }
    187 
    188 #[cfg(any(target_os = "linux", target_os = "macos"))]
    189 impl MycLocalSignerClient {
    190     /// Constructs a client from one validated local-signer provider binding.
    191     pub fn new(binding: &MycProviderBinding) -> Result<Self, MycLocalSignerTransportError> {
    192         use radroots_service_host::{AdminClient, AdminClientTarget, AdminTransportLimits};
    193 
    194         if binding.kind() != MycProviderKind::LocalSigner {
    195             return Err(transport_error(
    196                 MycLocalSignerTransportErrorKind::InvalidBinding,
    197             ));
    198         }
    199         let socket_path = binding
    200             .local_signer_socket_path()
    201             .ok_or_else(|| transport_error(MycLocalSignerTransportErrorKind::InvalidBinding))?;
    202         let limits = binding
    203             .local_signer_limits()
    204             .ok_or_else(|| transport_error(MycLocalSignerTransportErrorKind::InvalidBinding))?;
    205         let request_deadline = Duration::from_millis(limits.request_deadline_ms());
    206         let mut transport_values = AdminTransportLimits::DEFAULT.values();
    207         transport_values.request_body_utf8_bytes = u32::try_from(limits.request_max_bytes())
    208             .map_err(|_| transport_error(MycLocalSignerTransportErrorKind::InvalidLimits))?;
    209         transport_values.response_body_utf8_bytes = u32::try_from(limits.response_max_bytes())
    210             .map_err(|_| transport_error(MycLocalSignerTransportErrorKind::InvalidLimits))?;
    211         transport_values.concurrent_connections = limits.concurrency();
    212         transport_values.request_deadline = request_deadline;
    213         let transport_limits = AdminTransportLimits::new(transport_values)
    214             .map_err(|_| transport_error(MycLocalSignerTransportErrorKind::InvalidLimits))?;
    215         let transport = AdminClient::new(socket_path, transport_limits)
    216             .map_err(|_| transport_error(MycLocalSignerTransportErrorKind::InvalidBinding))?;
    217         let target = AdminClientTarget::new(MYC_LOCAL_SIGNER_ENDPOINT)
    218             .map_err(|_| transport_error(MycLocalSignerTransportErrorKind::InvalidBinding))?;
    219         Ok(Self {
    220             transport,
    221             target,
    222             permits: tokio::sync::Semaphore::new(limits.concurrency() as usize),
    223             request_deadline,
    224             role: binding.role(),
    225             instance: binding.instance(),
    226             expected_identity: binding.expected_identity().clone(),
    227         })
    228     }
    229 
    230     /// Executes one already-bound operation and returns only untrusted output.
    231     pub async fn execute(
    232         &self,
    233         operation: &MycProviderOperation,
    234     ) -> Result<MycLocalSignerUntrustedResponse, MycLocalSignerTransportError> {
    235         use radroots_service_host::{AdminCorrelationId, AdminOperationId};
    236 
    237         self.validate_operation(operation)?;
    238         let request = LocalSignerRequest::from_operation(operation)?;
    239         let operation_id = AdminOperationId::new(hex::encode(operation.operation_id().as_bytes()))
    240             .map_err(|_| transport_error(MycLocalSignerTransportErrorKind::InvalidOperation))?;
    241         let correlation_id =
    242             AdminCorrelationId::new(hex::encode(operation.correlation_id().as_bytes()))
    243                 .map_err(|_| transport_error(MycLocalSignerTransportErrorKind::InvalidOperation))?;
    244         let response = tokio::time::timeout(self.request_deadline, async {
    245             let _permit = self
    246                 .permits
    247                 .acquire()
    248                 .await
    249                 .map_err(|_| transport_error(MycLocalSignerTransportErrorKind::Transport))?;
    250             self.transport
    251                 .mutate::<_, LocalSignerResponse>(
    252                     &self.target,
    253                     operation_id,
    254                     Some(correlation_id.clone()),
    255                     request,
    256                 )
    257                 .await
    258                 .map_err(map_admin_error)
    259         })
    260         .await
    261         .map_err(|_| transport_error(MycLocalSignerTransportErrorKind::Deadline))??;
    262         Ok(MycLocalSignerUntrustedResponse {
    263             outer_correlation_id: response.correlation_id().as_str().into(),
    264             response: response.into_result(),
    265         })
    266     }
    267 
    268     fn validate_operation(
    269         &self,
    270         operation: &MycProviderOperation,
    271     ) -> Result<(), MycLocalSignerTransportError> {
    272         if operation.provider() != MycProviderKind::LocalSigner
    273             || operation.role() != self.role
    274             || operation.instance() != self.instance
    275             || operation.expected_identity() != &self.expected_identity
    276         {
    277             return Err(transport_error(
    278                 MycLocalSignerTransportErrorKind::InvalidOperation,
    279             ));
    280         }
    281         Ok(())
    282     }
    283 }
    284 
    285 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
    286 impl MycLocalSignerClient {
    287     /// Fails closed before any transport work on unsupported targets.
    288     pub fn new(_binding: &MycProviderBinding) -> Result<Self, MycLocalSignerTransportError> {
    289         Err(transport_error(
    290             MycLocalSignerTransportErrorKind::UnsupportedPlatform,
    291         ))
    292     }
    293 
    294     /// Fails closed before any transport work on unsupported targets.
    295     pub async fn execute(
    296         &self,
    297         _operation: &MycProviderOperation,
    298     ) -> Result<MycLocalSignerUntrustedResponse, MycLocalSignerTransportError> {
    299         Err(transport_error(
    300             MycLocalSignerTransportErrorKind::UnsupportedPlatform,
    301         ))
    302     }
    303 }
    304 
    305 impl fmt::Debug for MycLocalSignerClient {
    306     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    307         formatter
    308             .debug_struct("MycLocalSignerClient")
    309             .field("socket_path", &"[redacted]")
    310             .finish_non_exhaustive()
    311     }
    312 }
    313 
    314 #[cfg(any(target_os = "linux", target_os = "macos"))]
    315 fn map_admin_error(error: radroots_service_host::AdminClientError) -> MycLocalSignerTransportError {
    316     use radroots_service_host::AdminClientErrorKind;
    317 
    318     let kind = match error.kind() {
    319         AdminClientErrorKind::SocketPath => MycLocalSignerTransportErrorKind::InvalidBinding,
    320         AdminClientErrorKind::RequestEncoding => MycLocalSignerTransportErrorKind::RequestEncoding,
    321         AdminClientErrorKind::RequestLimit => MycLocalSignerTransportErrorKind::RequestLimit,
    322         AdminClientErrorKind::Deadline => MycLocalSignerTransportErrorKind::Deadline,
    323         AdminClientErrorKind::Connect | AdminClientErrorKind::Transport => {
    324             MycLocalSignerTransportErrorKind::Transport
    325         }
    326         AdminClientErrorKind::QueryLimit
    327         | AdminClientErrorKind::ResponseHeaders
    328         | AdminClientErrorKind::ResponseLimit
    329         | AdminClientErrorKind::ResponseContentType
    330         | AdminClientErrorKind::ResponseHttpVersion
    331         | AdminClientErrorKind::MalformedResponse
    332         | AdminClientErrorKind::UnsupportedContractVersion => {
    333             MycLocalSignerTransportErrorKind::Response
    334         }
    335         AdminClientErrorKind::ServerFailure => MycLocalSignerTransportErrorKind::RemoteFailure,
    336     };
    337     transport_error(kind)
    338 }
    339 
    340 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
    341 #[serde(rename_all = "snake_case")]
    342 pub(crate) enum WireRole {
    343     Transport,
    344     User,
    345     Discovery,
    346 }
    347 
    348 impl From<MycProviderRole> for WireRole {
    349     fn from(role: MycProviderRole) -> Self {
    350         match role {
    351             MycProviderRole::Transport => Self::Transport,
    352             MycProviderRole::User => Self::User,
    353             MycProviderRole::Discovery => Self::Discovery,
    354         }
    355     }
    356 }
    357 
    358 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
    359 #[serde(rename_all = "snake_case")]
    360 pub(crate) enum WireProviderInstance {
    361     Transport,
    362     User,
    363     Discovery,
    364 }
    365 
    366 impl From<MycProviderInstanceId> for WireProviderInstance {
    367     fn from(instance: MycProviderInstanceId) -> Self {
    368         match instance {
    369             MycProviderInstanceId::Transport => Self::Transport,
    370             MycProviderInstanceId::User => Self::User,
    371             MycProviderInstanceId::Discovery => Self::Discovery,
    372         }
    373     }
    374 }
    375 
    376 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
    377 #[serde(rename_all = "snake_case")]
    378 pub(crate) enum WireCapability {
    379     Describe,
    380     PublicIdentity,
    381     SignEvent,
    382     Nip04Encrypt,
    383     Nip04Decrypt,
    384     Nip44Encrypt,
    385     Nip44Decrypt,
    386 }
    387 
    388 impl From<MycProviderCapability> for WireCapability {
    389     fn from(capability: MycProviderCapability) -> Self {
    390         match capability {
    391             MycProviderCapability::Describe => Self::Describe,
    392             MycProviderCapability::PublicIdentity => Self::PublicIdentity,
    393             MycProviderCapability::SignEvent => Self::SignEvent,
    394             MycProviderCapability::Nip04Encrypt => Self::Nip04Encrypt,
    395             MycProviderCapability::Nip04Decrypt => Self::Nip04Decrypt,
    396             MycProviderCapability::Nip44Encrypt => Self::Nip44Encrypt,
    397             MycProviderCapability::Nip44Decrypt => Self::Nip44Decrypt,
    398         }
    399     }
    400 }
    401 
    402 #[cfg(any(test, target_os = "linux", target_os = "macos"))]
    403 #[derive(Serialize, Deserialize)]
    404 #[serde(deny_unknown_fields)]
    405 struct LocalSignerRequest {
    406     contract_version: u32,
    407     provider_instance: WireProviderInstance,
    408     role: WireRole,
    409     operation_id: String,
    410     correlation_id: String,
    411     absolute_deadline_unix_ms: u64,
    412     expected_identity: String,
    413     capability: WireCapability,
    414     input: WireProviderInput,
    415 }
    416 
    417 #[cfg(any(test, target_os = "linux", target_os = "macos"))]
    418 impl LocalSignerRequest {
    419     fn from_operation(
    420         operation: &MycProviderOperation,
    421     ) -> Result<Self, MycLocalSignerTransportError> {
    422         Ok(Self {
    423             contract_version: MYC_LOCAL_SIGNER_TRANSPORT_CONTRACT_VERSION,
    424             provider_instance: operation.instance().into(),
    425             role: operation.role().into(),
    426             operation_id: hex::encode(operation.operation_id().as_bytes()),
    427             correlation_id: hex::encode(operation.correlation_id().as_bytes()),
    428             absolute_deadline_unix_ms: operation.deadline().get(),
    429             expected_identity: operation.expected_identity().as_hex().to_owned(),
    430             capability: operation.input().capability().into(),
    431             input: WireProviderInput::from_operation(operation)?,
    432         })
    433     }
    434 }
    435 
    436 #[cfg(any(test, target_os = "linux", target_os = "macos"))]
    437 #[derive(Serialize, Deserialize)]
    438 #[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
    439 enum WireProviderInput {
    440     Describe,
    441     PublicIdentity,
    442     SignEvent {
    443         payload_hex: ProtectedWireHex,
    444     },
    445     Nip04Encrypt {
    446         peer: String,
    447         payload_hex: ProtectedWireHex,
    448     },
    449     Nip04Decrypt {
    450         peer: String,
    451         payload_hex: ProtectedWireHex,
    452     },
    453     Nip44Encrypt {
    454         peer: String,
    455         version: u8,
    456         payload_hex: ProtectedWireHex,
    457     },
    458     Nip44Decrypt {
    459         peer: String,
    460         version: u8,
    461         payload_hex: ProtectedWireHex,
    462     },
    463 }
    464 
    465 #[cfg(any(test, target_os = "linux", target_os = "macos"))]
    466 impl WireProviderInput {
    467     fn from_operation(
    468         operation: &MycProviderOperation,
    469     ) -> Result<Self, MycLocalSignerTransportError> {
    470         let input = operation.input();
    471         let protected = || {
    472             input
    473                 .bytes()
    474                 .map(ProtectedWireHex::from_bytes)
    475                 .ok_or_else(|| transport_error(MycLocalSignerTransportErrorKind::InvalidOperation))
    476         };
    477         let peer = || {
    478             input
    479                 .peer()
    480                 .map(|identity| identity.as_hex().to_owned())
    481                 .ok_or_else(|| transport_error(MycLocalSignerTransportErrorKind::InvalidOperation))
    482         };
    483         let version = || {
    484             input
    485                 .nip44_version()
    486                 .map(MycProviderNip44Version::as_u8)
    487                 .ok_or_else(|| transport_error(MycLocalSignerTransportErrorKind::InvalidOperation))
    488         };
    489         match input.capability() {
    490             MycProviderCapability::Describe => Ok(Self::Describe),
    491             MycProviderCapability::PublicIdentity => Ok(Self::PublicIdentity),
    492             MycProviderCapability::SignEvent => Ok(Self::SignEvent {
    493                 payload_hex: protected()?,
    494             }),
    495             MycProviderCapability::Nip04Encrypt => Ok(Self::Nip04Encrypt {
    496                 peer: peer()?,
    497                 payload_hex: protected()?,
    498             }),
    499             MycProviderCapability::Nip04Decrypt => Ok(Self::Nip04Decrypt {
    500                 peer: peer()?,
    501                 payload_hex: protected()?,
    502             }),
    503             MycProviderCapability::Nip44Encrypt => Ok(Self::Nip44Encrypt {
    504                 peer: peer()?,
    505                 version: version()?,
    506                 payload_hex: protected()?,
    507             }),
    508             MycProviderCapability::Nip44Decrypt => Ok(Self::Nip44Decrypt {
    509                 peer: peer()?,
    510                 version: version()?,
    511                 payload_hex: protected()?,
    512             }),
    513         }
    514     }
    515 }
    516 
    517 #[derive(Serialize, Deserialize)]
    518 #[serde(deny_unknown_fields)]
    519 pub(crate) struct LocalSignerResponse {
    520     pub(crate) contract_version: u32,
    521     pub(crate) provider_instance: WireProviderInstance,
    522     pub(crate) role: WireRole,
    523     pub(crate) operation_id: String,
    524     pub(crate) correlation_id: String,
    525     pub(crate) absolute_deadline_unix_ms: u64,
    526     pub(crate) expected_identity: String,
    527     pub(crate) capability: WireCapability,
    528     pub(crate) result: WireProviderResult,
    529 }
    530 
    531 #[derive(Serialize, Deserialize)]
    532 #[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
    533 pub(crate) enum WireProviderResult {
    534     Describe {
    535         public_identity: String,
    536         protocol_version: u32,
    537         capabilities: Vec<WireCapability>,
    538         maximum_request_bytes: u64,
    539     },
    540     PublicIdentity {
    541         public_identity: String,
    542     },
    543     SignEvent {
    544         payload_hex: ProtectedWireHex,
    545     },
    546     Nip04Encrypt {
    547         peer: String,
    548         payload_hex: ProtectedWireHex,
    549     },
    550     Nip04Decrypt {
    551         peer: String,
    552         payload_hex: ProtectedWireHex,
    553     },
    554     Nip44Encrypt {
    555         peer: String,
    556         version: u8,
    557         payload_hex: ProtectedWireHex,
    558     },
    559     Nip44Decrypt {
    560         peer: String,
    561         version: u8,
    562         payload_hex: ProtectedWireHex,
    563     },
    564 }
    565 
    566 pub(crate) struct ProtectedWireHex(Zeroizing<String>);
    567 
    568 impl ProtectedWireHex {
    569     pub(crate) fn from_bytes(bytes: &[u8]) -> Self {
    570         Self(Zeroizing::new(hex::encode(bytes)))
    571     }
    572 
    573     fn from_string(value: String) -> Result<Self, MycLocalSignerTransportError> {
    574         if value.len() > PROTECTED_WIRE_HEX_MAX_UTF8_BYTES
    575             || !value.len().is_multiple_of(2)
    576             || value
    577                 .bytes()
    578                 .any(|byte| !matches!(byte, b'0'..=b'9' | b'a'..=b'f'))
    579         {
    580             return Err(transport_error(MycLocalSignerTransportErrorKind::Response));
    581         }
    582         Ok(Self(Zeroizing::new(value)))
    583     }
    584 
    585     pub(crate) fn into_bytes(self) -> Result<Zeroizing<Vec<u8>>, MycLocalSignerTransportError> {
    586         hex::decode(self.0.as_bytes())
    587             .map(Zeroizing::new)
    588             .map_err(|_| transport_error(MycLocalSignerTransportErrorKind::Response))
    589     }
    590 }
    591 
    592 impl Serialize for ProtectedWireHex {
    593     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    594     where
    595         S: Serializer,
    596     {
    597         serializer.serialize_str(&self.0)
    598     }
    599 }
    600 
    601 impl<'de> Deserialize<'de> for ProtectedWireHex {
    602     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    603     where
    604         D: Deserializer<'de>,
    605     {
    606         struct Visitor;
    607 
    608         impl de::Visitor<'_> for Visitor {
    609             type Value = ProtectedWireHex;
    610 
    611             fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    612                 formatter.write_str("bounded lowercase even-length hexadecimal")
    613             }
    614 
    615             fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
    616             where
    617                 E: de::Error,
    618             {
    619                 if value.len() > PROTECTED_WIRE_HEX_MAX_UTF8_BYTES {
    620                     return Err(E::custom("protected wire value exceeds its bound"));
    621                 }
    622                 ProtectedWireHex::from_string(value.to_owned()).map_err(E::custom)
    623             }
    624 
    625             fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
    626             where
    627                 E: de::Error,
    628             {
    629                 ProtectedWireHex::from_string(value).map_err(E::custom)
    630             }
    631         }
    632 
    633         deserializer.deserialize_string(Visitor)
    634     }
    635 }
    636 
    637 #[cfg(test)]
    638 mod tests {
    639     use std::path::Path;
    640 
    641     use crate::{
    642         MycConfigProfile, MycProviderCorrelationId, MycProviderDeadlineUnixMs,
    643         MycProviderOperationId, MycProviderOperationInput, MycProviderPublicIdentity,
    644         parse_myc_config_v1,
    645     };
    646 
    647     use super::*;
    648 
    649     const CONFIG: &str = include_str!("../contracts/services_hardening/config.v1.example.toml");
    650 
    651     fn user_binding(socket: &Path, deadline_ms: u64, concurrency: u32) -> MycProviderBinding {
    652         let source = CONFIG
    653             .replace(
    654                 "/run/radroots/services/myc/primary/user-signer.sock",
    655                 socket.to_str().expect("UTF-8 test socket"),
    656             )
    657             .replacen(
    658                 "request_deadline_ms = 15000",
    659                 &format!("request_deadline_ms = {deadline_ms}"),
    660                 1,
    661             )
    662             .replacen(
    663                 "concurrency = 32",
    664                 &format!("concurrency = {concurrency}"),
    665                 1,
    666             );
    667         parse_myc_config_v1(source.as_bytes(), MycConfigProfile::RepoLocal)
    668             .expect("test configuration")
    669             .provider_contract()
    670             .binding(MycProviderRole::User)
    671             .expect("user binding")
    672             .clone()
    673     }
    674 
    675     fn operation(
    676         binding: &MycProviderBinding,
    677         seed: u8,
    678         input: MycProviderOperationInput,
    679     ) -> MycProviderOperation {
    680         MycProviderOperation::new(
    681             binding,
    682             MycProviderOperationId::from_bytes([seed; 32]),
    683             MycProviderCorrelationId::from_bytes([seed.wrapping_add(1); 32]),
    684             MycProviderDeadlineUnixMs::new(2_000_000_000_000).expect("deadline"),
    685             input,
    686         )
    687         .expect("operation")
    688     }
    689 
    690     #[test]
    691     fn protected_wire_hex_is_bounded_canonical_and_redacted() {
    692         let value = ProtectedWireHex::from_bytes(b"protected-value");
    693         assert_eq!(
    694             serde_json::to_string(&value).unwrap(),
    695             format!("\"{}\"", hex::encode(b"protected-value"))
    696         );
    697         for invalid in ["a", "AA", "0g"] {
    698             assert!(serde_json::from_str::<ProtectedWireHex>(&format!("\"{invalid}\"")).is_err());
    699         }
    700         let oversized = format!("\"{}\"", "a".repeat(PROTECTED_WIRE_HEX_MAX_UTF8_BYTES + 2));
    701         assert!(serde_json::from_str::<ProtectedWireHex>(&oversized).is_err());
    702     }
    703 
    704     #[test]
    705     fn transport_errors_are_source_free_and_fixed() {
    706         for kind in [
    707             MycLocalSignerTransportErrorKind::InvalidBinding,
    708             MycLocalSignerTransportErrorKind::InvalidOperation,
    709             MycLocalSignerTransportErrorKind::InvalidLimits,
    710             MycLocalSignerTransportErrorKind::RequestEncoding,
    711             MycLocalSignerTransportErrorKind::RequestLimit,
    712             MycLocalSignerTransportErrorKind::Transport,
    713             MycLocalSignerTransportErrorKind::Deadline,
    714             MycLocalSignerTransportErrorKind::Response,
    715             MycLocalSignerTransportErrorKind::RemoteFailure,
    716             MycLocalSignerTransportErrorKind::UnsupportedPlatform,
    717         ] {
    718             let error = transport_error(kind);
    719             assert_eq!(error.kind(), kind);
    720             assert!(!error.code().is_empty());
    721             assert!(error.source().is_none());
    722         }
    723     }
    724 
    725     #[test]
    726     fn every_operation_has_one_closed_internal_tag_and_complete_binding() {
    727         let binding = user_binding(Path::new("/run/test-signer.sock"), 15_000, 1);
    728         let peer = MycProviderPublicIdentity::new(
    729             "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
    730         )
    731         .expect("peer");
    732         let inputs = [
    733             MycProviderOperationInput::describe(),
    734             MycProviderOperationInput::public_identity(),
    735             MycProviderOperationInput::sign_event(b"unsigned-event").expect("sign"),
    736             MycProviderOperationInput::nip04_encrypt(peer.clone(), b"plaintext")
    737                 .expect("nip04 encrypt"),
    738             MycProviderOperationInput::nip04_decrypt(peer.clone(), b"ciphertext")
    739                 .expect("nip04 decrypt"),
    740             MycProviderOperationInput::nip44_encrypt(
    741                 peer.clone(),
    742                 MycProviderNip44Version::V2,
    743                 b"plaintext",
    744             )
    745             .expect("nip44 encrypt"),
    746             MycProviderOperationInput::nip44_decrypt(
    747                 peer,
    748                 MycProviderNip44Version::V2,
    749                 b"ciphertext",
    750             )
    751             .expect("nip44 decrypt"),
    752         ];
    753         let expected = [
    754             "describe",
    755             "public_identity",
    756             "sign_event",
    757             "nip04_encrypt",
    758             "nip04_decrypt",
    759             "nip44_encrypt",
    760             "nip44_decrypt",
    761         ];
    762         for (index, (input, expected)) in inputs.into_iter().zip(expected).enumerate() {
    763             let operation = operation(&binding, index as u8 + 1, input);
    764             let request = LocalSignerRequest::from_operation(&operation).expect("wire request");
    765             let value = serde_json::to_value(request).expect("request JSON");
    766             assert_eq!(value["contract_version"], 1);
    767             assert_eq!(value["provider_instance"], "user");
    768             assert_eq!(value["role"], "user");
    769             assert_eq!(value["capability"], expected);
    770             assert_eq!(value["input"]["type"], expected);
    771             assert_eq!(value["operation_id"].as_str().map(str::len), Some(64));
    772             assert_eq!(value["correlation_id"].as_str().map(str::len), Some(64));
    773             assert_eq!(value["absolute_deadline_unix_ms"], 2_000_000_000_000_u64);
    774             assert_eq!(
    775                 value["expected_identity"],
    776                 binding.expected_identity().as_hex()
    777             );
    778         }
    779     }
    780 
    781     #[cfg(any(target_os = "linux", target_os = "macos"))]
    782     mod native {
    783         use core::sync::atomic::{AtomicUsize, Ordering};
    784         use std::sync::Arc;
    785 
    786         use radroots_service_host::{
    787             AdminHttpMethod, AdminMutationRequest, AdminRouter, AdminServer, AdminTransportLimits,
    788             CancellationToken, EntropyError, EntropySource, UnixAdminSocketBinding,
    789             UnixAdminSocketWriterAuthority,
    790         };
    791 
    792         use super::*;
    793 
    794         struct FixedEntropy;
    795 
    796         impl EntropySource for FixedEntropy {
    797             fn fill_bytes(&self, destination: &mut [u8]) -> Result<(), EntropyError> {
    798                 destination.fill(7);
    799                 Ok(())
    800             }
    801         }
    802 
    803         fn runtime_directory() -> tempfile::TempDir {
    804             tempfile::Builder::new()
    805                 .prefix("myc-ls-")
    806                 .tempdir_in("/tmp")
    807                 .expect("short runtime directory")
    808         }
    809 
    810         async fn start_server<F, Fut>(
    811             directory: &tempfile::TempDir,
    812             handler: F,
    813         ) -> (
    814             std::path::PathBuf,
    815             CancellationToken,
    816             tokio::task::JoinHandle<()>,
    817         )
    818         where
    819             F: Fn(radroots_service_host::AdminRequest) -> Fut + Send + Sync + 'static,
    820             Fut: core::future::Future<Output = radroots_service_host::AdminRouteOutcome>
    821                 + Send
    822                 + 'static,
    823         {
    824             let socket = directory.path().join("user-signer.sock");
    825             let authority = UnixAdminSocketWriterAuthority::acquire(directory.path())
    826                 .expect("writer authority");
    827             let binding = UnixAdminSocketBinding::bind(authority, &socket)
    828                 .await
    829                 .expect("socket binding");
    830             let mut router = AdminRouter::new();
    831             router
    832                 .route(AdminHttpMethod::Post, MYC_LOCAL_SIGNER_ENDPOINT, handler)
    833                 .expect("provider route");
    834             let server = AdminServer::new(router, AdminTransportLimits::DEFAULT, FixedEntropy)
    835                 .expect("admin server");
    836             let cancellation = CancellationToken::new();
    837             let server_cancellation = cancellation.clone();
    838             let task = tokio::spawn(async move {
    839                 server
    840                     .serve(binding, server_cancellation)
    841                     .await
    842                     .expect("serve local signer");
    843             });
    844             (socket, cancellation, task)
    845         }
    846 
    847         fn response_from(request: LocalSignerRequest) -> LocalSignerResponse {
    848             LocalSignerResponse {
    849                 contract_version: request.contract_version,
    850                 provider_instance: request.provider_instance,
    851                 role: request.role,
    852                 operation_id: request.operation_id,
    853                 correlation_id: request.correlation_id,
    854                 absolute_deadline_unix_ms: request.absolute_deadline_unix_ms,
    855                 expected_identity: request.expected_identity.clone(),
    856                 capability: request.capability,
    857                 result: WireProviderResult::PublicIdentity {
    858                     public_identity: request.expected_identity,
    859                 },
    860             }
    861         }
    862 
    863         #[tokio::test]
    864         async fn hardened_admin_round_trip_carries_exact_bound_operation() {
    865             let directory = runtime_directory();
    866             let (socket, cancellation, task) = start_server(&directory, |request| async move {
    867                 let envelope = request
    868                     .decode_json::<AdminMutationRequest<LocalSignerRequest>>()
    869                     .expect("strict mutation request");
    870                 let response = response_from(envelope.into_request());
    871                 request.success(&response).expect("provider response")
    872             })
    873             .await;
    874             let binding = user_binding(&socket, 1_000, 2);
    875             let operation = operation(&binding, 4, MycProviderOperationInput::public_identity());
    876             let client = MycLocalSignerClient::new(&binding).expect("client");
    877             let response = client.execute(&operation).await.expect("transport success");
    878             assert_eq!(
    879                 response.outer_correlation_id.as_ref(),
    880                 hex::encode(operation.correlation_id().as_bytes())
    881             );
    882             assert_eq!(response.response.contract_version, 1);
    883             assert_eq!(
    884                 response.response.provider_instance,
    885                 WireProviderInstance::User
    886             );
    887             assert_eq!(response.response.role, WireRole::User);
    888             assert_eq!(response.response.capability, WireCapability::PublicIdentity);
    889             assert_eq!(
    890                 format!("{response:?}"),
    891                 "MycLocalSignerUntrustedResponse { correlation_id: \"[redacted]\", provider_instance: User, role: User, capability: PublicIdentity, result: \"[redacted]\" }"
    892             );
    893             cancellation.cancel();
    894             task.await.expect("server task");
    895         }
    896 
    897         #[tokio::test]
    898         async fn malformed_success_shape_is_normalized_without_raw_response_exposure() {
    899             let directory = runtime_directory();
    900             let (socket, cancellation, task) = start_server(&directory, |request| async move {
    901                 let _: AdminMutationRequest<LocalSignerRequest> =
    902                     request.decode_json().expect("strict mutation request");
    903                 request
    904                     .success(&serde_json::json!({"unexpected": "protected-response"}))
    905                     .expect("malformed semantic success")
    906             })
    907             .await;
    908             let binding = user_binding(&socket, 1_000, 2);
    909             let operation = operation(&binding, 5, MycProviderOperationInput::public_identity());
    910             let client = MycLocalSignerClient::new(&binding).expect("client");
    911             let error = client
    912                 .execute(&operation)
    913                 .await
    914                 .expect_err("malformed response");
    915             assert_eq!(error.kind(), MycLocalSignerTransportErrorKind::Response);
    916             assert!(!format!("{error} {error:?}").contains("protected-response"));
    917             cancellation.cancel();
    918             task.await.expect("server task");
    919         }
    920 
    921         #[tokio::test]
    922         async fn configured_deadline_and_per_client_concurrency_are_enforced() {
    923             let directory = runtime_directory();
    924             let active = Arc::new(AtomicUsize::new(0));
    925             let maximum = Arc::new(AtomicUsize::new(0));
    926             let handler_active = Arc::clone(&active);
    927             let handler_maximum = Arc::clone(&maximum);
    928             let (socket, cancellation, task) = start_server(&directory, move |request| {
    929                 let active = Arc::clone(&handler_active);
    930                 let maximum = Arc::clone(&handler_maximum);
    931                 async move {
    932                     let current = active.fetch_add(1, Ordering::SeqCst) + 1;
    933                     maximum.fetch_max(current, Ordering::SeqCst);
    934                     tokio::time::sleep(Duration::from_millis(30)).await;
    935                     active.fetch_sub(1, Ordering::SeqCst);
    936                     let envelope = request
    937                         .decode_json::<AdminMutationRequest<LocalSignerRequest>>()
    938                         .expect("strict request");
    939                     request
    940                         .success(&response_from(envelope.into_request()))
    941                         .expect("response")
    942                 }
    943             })
    944             .await;
    945             let binding = user_binding(&socket, 500, 1);
    946             let client = Arc::new(MycLocalSignerClient::new(&binding).expect("client"));
    947             let first = operation(&binding, 1, MycProviderOperationInput::public_identity());
    948             let second = operation(&binding, 2, MycProviderOperationInput::public_identity());
    949             let first_client = Arc::clone(&client);
    950             let second_client = Arc::clone(&client);
    951             let (first_result, second_result) = tokio::join!(
    952                 async move { first_client.execute(&first).await },
    953                 async move { second_client.execute(&second).await }
    954             );
    955             first_result.expect("first response");
    956             second_result.expect("second response");
    957             assert_eq!(maximum.load(Ordering::SeqCst), 1);
    958             cancellation.cancel();
    959             task.await.expect("server task");
    960 
    961             let directory = runtime_directory();
    962             let (socket, cancellation, task) = start_server(&directory, |request| async move {
    963                 tokio::time::sleep(Duration::from_millis(75)).await;
    964                 let envelope = request
    965                     .decode_json::<AdminMutationRequest<LocalSignerRequest>>()
    966                     .expect("strict request");
    967                 request
    968                     .success(&response_from(envelope.into_request()))
    969                     .expect("response")
    970             })
    971             .await;
    972             let binding = user_binding(&socket, 10, 1);
    973             let client = MycLocalSignerClient::new(&binding).expect("deadline client");
    974             let operation = operation(&binding, 3, MycProviderOperationInput::public_identity());
    975             assert_eq!(
    976                 client
    977                     .execute(&operation)
    978                     .await
    979                     .expect_err("deadline")
    980                     .kind(),
    981                 MycLocalSignerTransportErrorKind::Deadline
    982             );
    983             cancellation.cancel();
    984             task.await.expect("server task");
    985         }
    986 
    987         #[tokio::test]
    988         async fn wrong_provider_and_request_limit_fail_before_transport_success() {
    989             let socket = Path::new("/run/test-signer.sock");
    990             let mut source = CONFIG
    991                 .replace(
    992                     "/run/radroots/services/myc/primary/user-signer.sock",
    993                     socket.to_str().expect("UTF-8 socket"),
    994                 )
    995                 .replacen("request_max_bytes = 65536", "request_max_bytes = 1", 1);
    996             source = source.replacen("concurrency = 32", "concurrency = 1", 1);
    997             let tiny = parse_myc_config_v1(source.as_bytes(), MycConfigProfile::RepoLocal)
    998                 .expect("tiny configuration")
    999                 .provider_contract()
   1000                 .binding(MycProviderRole::User)
   1001                 .expect("tiny binding")
   1002                 .clone();
   1003             let client = MycLocalSignerClient::new(&tiny).expect("tiny client");
   1004             assert_eq!(client.transport.limits().request_body_utf8_bytes(), 1);
   1005             assert_eq!(
   1006                 client.transport.limits().response_body_utf8_bytes(),
   1007                 1_048_576
   1008             );
   1009             assert_eq!(client.transport.limits().concurrent_connections(), 1);
   1010             assert_eq!(
   1011                 client.transport.limits().request_deadline(),
   1012                 Duration::from_secs(15)
   1013             );
   1014             assert_eq!(client.permits.available_permits(), 1);
   1015             let tiny_operation = operation(&tiny, 9, MycProviderOperationInput::public_identity());
   1016             assert_eq!(
   1017                 client
   1018                     .execute(&tiny_operation)
   1019                     .await
   1020                     .expect_err("request bound")
   1021                     .kind(),
   1022                 MycLocalSignerTransportErrorKind::RequestLimit
   1023             );
   1024 
   1025             let encrypted = parse_myc_config_v1(CONFIG.as_bytes(), MycConfigProfile::RepoLocal)
   1026                 .expect("configuration")
   1027                 .provider_contract()
   1028                 .binding(MycProviderRole::Transport)
   1029                 .expect("encrypted binding")
   1030                 .clone();
   1031             assert_eq!(
   1032                 MycLocalSignerClient::new(&encrypted)
   1033                     .expect_err("wrong provider")
   1034                     .kind(),
   1035                 MycLocalSignerTransportErrorKind::InvalidBinding
   1036             );
   1037 
   1038             let mismatched =
   1039                 operation(&encrypted, 10, MycProviderOperationInput::public_identity());
   1040             assert_eq!(
   1041                 client
   1042                     .execute(&mismatched)
   1043                     .await
   1044                     .expect_err("mismatched operation")
   1045                     .kind(),
   1046                 MycLocalSignerTransportErrorKind::InvalidOperation
   1047             );
   1048         }
   1049     }
   1050 }