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 }