state_metadata.rs (22845B)
1 //! Immutable Myc-specific identity and policy evidence for one state host. 2 3 use core::fmt; 4 use std::error::Error; 5 6 use nostr::PublicKey; 7 use radroots_service_sqlite::{ 8 ServiceDatabaseIdentity, ServiceDatabaseMetadata, ServiceSqliteApplicationId, 9 ServiceSqlitePaths, 10 }; 11 use radroots_storage::event::SourceGeneration; 12 use sha2::{Digest, Sha256}; 13 14 use crate::nip46_authorization::MycConnectionPolicies; 15 use crate::state_connection::{ 16 MycAuthorizationChallengeRecord, MycAuthorizationChallengeRequest, 17 MycConnectionAdmissionRequest, MycConnectionOperatorDecision, MycConnectionTimeUnixMs, 18 }; 19 use crate::state_delivery::{MycDeliveryPolicies, MycDeliveryPolicyMode, MycDeliveryRelayId}; 20 use crate::state_discovery::MycDiscoveryPolicies; 21 use crate::state_governance::{ 22 MycGovernancePolicies, MycRateLimitClass, MycRateLimitPolicy, MycRateRelayId, 23 }; 24 use crate::{ 25 MYC_CONFIG_SCHEMA_VERSION, MYC_STATE_BASE_SCHEMA_VERSION, MYC_STATE_SCHEMA_VERSION, 26 MycBootstrapProfileV1, MycConfigDocumentV1, MycConfigProfile, MycRuntimeContext, 27 }; 28 29 const NORMALIZED_CONFIG_DIGEST_DOMAIN: &[u8] = b"radroots.myc.normalized_config.v1\0"; 30 31 /// SQLite application identity for Myc, encoded as ASCII `RDMY`. 32 pub const MYC_STATE_APPLICATION_ID: u32 = 0x5244_4d59; 33 34 /// Exact version of the governed Myc operator contract. 35 pub const MYC_OPERATOR_CONTRACT_VERSION: u32 = 1; 36 /// Exact version of the governed Myc status contract. 37 pub const MYC_SIGNER_STATUS_CONTRACT_VERSION: u32 = 1; 38 39 /// SHA-256 identity of one fully defaulted normalized Myc configuration. 40 #[derive(Clone, Copy, PartialEq, Eq, Hash)] 41 pub struct MycNormalizedConfigDigest([u8; 32]); 42 43 impl MycNormalizedConfigDigest { 44 /// Returns the exact digest bytes. 45 #[must_use] 46 pub const fn as_bytes(&self) -> &[u8; 32] { 47 &self.0 48 } 49 } 50 51 impl fmt::Debug for MycNormalizedConfigDigest { 52 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 53 formatter.write_str("MycNormalizedConfigDigest([redacted])") 54 } 55 } 56 57 /// One validated canonical expected Nostr public identity. 58 #[derive(Clone, PartialEq, Eq, Hash)] 59 pub struct MycExpectedPublicIdentity(Box<str>); 60 61 impl MycExpectedPublicIdentity { 62 /// Returns the canonical lowercase 32-byte x-only public key in hex. 63 #[must_use] 64 pub fn as_hex(&self) -> &str { 65 &self.0 66 } 67 68 fn from_hex(value: &str) -> Result<Self, MycStateMetadataError> { 69 let public_key = PublicKey::from_hex(value) 70 .map_err(|_| MycStateMetadataError::new(MycStateMetadataErrorKind::Identity))?; 71 public_key 72 .xonly() 73 .map_err(|_| MycStateMetadataError::new(MycStateMetadataErrorKind::Identity))?; 74 let canonical = public_key.to_hex(); 75 if canonical != value { 76 return Err(MycStateMetadataError::new( 77 MycStateMetadataErrorKind::Identity, 78 )); 79 } 80 Ok(Self(canonical.into_boxed_str())) 81 } 82 } 83 84 impl fmt::Debug for MycExpectedPublicIdentity { 85 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 86 formatter.write_str("MycExpectedPublicIdentity([redacted])") 87 } 88 } 89 90 /// Exact expected identities for every enabled Myc role. 91 #[derive(Clone, PartialEq, Eq)] 92 pub struct MycExpectedIdentities { 93 transport: MycExpectedPublicIdentity, 94 user: MycExpectedPublicIdentity, 95 discovery: Option<MycExpectedPublicIdentity>, 96 } 97 98 impl MycExpectedIdentities { 99 /// Returns the required transport identity. 100 #[must_use] 101 pub const fn transport(&self) -> &MycExpectedPublicIdentity { 102 &self.transport 103 } 104 105 /// Returns the required user identity. 106 #[must_use] 107 pub const fn user(&self) -> &MycExpectedPublicIdentity { 108 &self.user 109 } 110 111 /// Returns the discovery identity only when discovery is enabled. 112 #[must_use] 113 pub const fn discovery(&self) -> Option<&MycExpectedPublicIdentity> { 114 self.discovery.as_ref() 115 } 116 } 117 118 impl fmt::Debug for MycExpectedIdentities { 119 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 120 formatter 121 .debug_struct("MycExpectedIdentities") 122 .field("transport", &"[redacted]") 123 .field("user", &"[redacted]") 124 .field("discovery", &self.discovery.as_ref().map(|_| "[redacted]")) 125 .finish() 126 } 127 } 128 129 /// Exact contract versions bound to one Myc state-host session. 130 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] 131 pub struct MycStatePolicyVersions { 132 configuration: u32, 133 state: u32, 134 operator: u32, 135 status: u32, 136 } 137 138 impl MycStatePolicyVersions { 139 const fn governed() -> Self { 140 Self { 141 configuration: MYC_CONFIG_SCHEMA_VERSION, 142 state: MYC_STATE_SCHEMA_VERSION, 143 operator: MYC_OPERATOR_CONTRACT_VERSION, 144 status: MYC_SIGNER_STATUS_CONTRACT_VERSION, 145 } 146 } 147 148 #[must_use] 149 /// Returns the exact configuration-contract version. 150 pub const fn configuration(self) -> u32 { 151 self.configuration 152 } 153 154 #[must_use] 155 /// Returns the exact state-schema version. 156 pub const fn state(self) -> u32 { 157 self.state 158 } 159 160 #[must_use] 161 /// Returns the exact operator-contract version. 162 pub const fn operator(self) -> u32 { 163 self.operator 164 } 165 166 #[must_use] 167 /// Returns the exact signer-status contract version. 168 pub const fn status(self) -> u32 { 169 self.status 170 } 171 } 172 173 /// Non-forgeable Myc metadata bound to one runtime context and configuration. 174 #[derive(Clone, PartialEq, Eq)] 175 pub struct MycStateMetadata { 176 paths: ServiceSqlitePaths, 177 database: ServiceDatabaseMetadata, 178 database_identity: ServiceDatabaseIdentity, 179 configuration: MycNormalizedConfigDigest, 180 identities: MycExpectedIdentities, 181 governance: MycGovernancePolicies, 182 authorization: MycConnectionPolicies, 183 delivery: MycDeliveryPolicies, 184 discovery: Option<MycDiscoveryPolicies>, 185 policy_versions: MycStatePolicyVersions, 186 } 187 188 impl MycStateMetadata { 189 /// Derives all state evidence from one sealed runtime context, one admitted 190 /// normalized configuration, and caller-injected generation/time evidence. 191 pub fn new( 192 runtime: &MycRuntimeContext, 193 configuration: &MycConfigDocumentV1, 194 source_generation: SourceGeneration, 195 created_at_unix_ms: u64, 196 ) -> Result<Self, MycStateMetadataError> { 197 require_profile_binding(runtime.profile(), configuration.profile())?; 198 let paths = ServiceSqlitePaths::from_runtime_context(runtime.context()) 199 .map_err(|_| MycStateMetadataError::new(MycStateMetadataErrorKind::Paths))?; 200 let application_id = ServiceSqliteApplicationId::new(MYC_STATE_APPLICATION_ID) 201 .map_err(|_| MycStateMetadataError::new(MycStateMetadataErrorKind::Invariant))?; 202 let state_schema_version = core::num::NonZeroU32::new(MYC_STATE_BASE_SCHEMA_VERSION) 203 .ok_or_else(|| MycStateMetadataError::new(MycStateMetadataErrorKind::Invariant))?; 204 let database = ServiceDatabaseMetadata::new( 205 &paths, 206 source_generation, 207 state_schema_version, 208 created_at_unix_ms, 209 application_id, 210 ) 211 .map_err(|_| MycStateMetadataError::new(MycStateMetadataErrorKind::Database))?; 212 let supported_state_schema_version = 213 core::num::NonZeroU32::new(MYC_STATE_SCHEMA_VERSION) 214 .ok_or_else(|| MycStateMetadataError::new(MycStateMetadataErrorKind::Invariant))?; 215 let database_identity = ServiceDatabaseIdentity::new( 216 &paths, 217 source_generation, 218 supported_state_schema_version, 219 application_id, 220 ); 221 let normalized = configuration.normalized(); 222 let governance = governance_policies(normalized)?; 223 let authorization = MycConnectionPolicies::from_normalized(normalized) 224 .map_err(|_| MycStateMetadataError::new(MycStateMetadataErrorKind::Invariant))?; 225 if !authorization.retention_is_bounded() { 226 return Err(MycStateMetadataError::new( 227 MycStateMetadataErrorKind::Invariant, 228 )); 229 } 230 let identities = expected_identities(normalized)?; 231 let delivery = delivery_policies(normalized)?; 232 let discovery = MycDiscoveryPolicies::from_normalized(normalized, &identities) 233 .map_err(|_| MycStateMetadataError::new(MycStateMetadataErrorKind::Invariant))?; 234 let configuration = normalized_config_digest(configuration.profile(), normalized)?; 235 let policy_versions = MycStatePolicyVersions::governed(); 236 if [ 237 policy_versions.configuration, 238 policy_versions.state, 239 policy_versions.operator, 240 policy_versions.status, 241 ] 242 .contains(&0) 243 { 244 return Err(MycStateMetadataError::new( 245 MycStateMetadataErrorKind::Invariant, 246 )); 247 } 248 Ok(Self { 249 paths, 250 database, 251 database_identity, 252 configuration, 253 identities, 254 governance, 255 authorization, 256 delivery, 257 discovery, 258 policy_versions, 259 }) 260 } 261 262 pub(crate) fn from_existing_database( 263 runtime: &MycRuntimeContext, 264 configuration: &MycConfigDocumentV1, 265 actual: &ServiceDatabaseMetadata, 266 ) -> Result<Self, MycStateMetadataError> { 267 let expected_application = ServiceSqliteApplicationId::new(MYC_STATE_APPLICATION_ID) 268 .map_err(|_| MycStateMetadataError::new(MycStateMetadataErrorKind::Invariant))?; 269 if actual.service() != runtime.context().service() 270 || actual.instance() != runtime.context().instance() 271 || actual.application_id() != expected_application 272 || actual.state_schema_version().get() < MYC_STATE_BASE_SCHEMA_VERSION 273 || actual.state_schema_version().get() > MYC_STATE_SCHEMA_VERSION 274 { 275 return Err(MycStateMetadataError::new( 276 MycStateMetadataErrorKind::Database, 277 )); 278 } 279 Self::new( 280 runtime, 281 configuration, 282 actual.source_generation(), 283 actual.created_at_unix_ms(), 284 ) 285 } 286 287 /// Returns the immutable shared schema-v1 initialization metadata. 288 #[must_use] 289 pub const fn initial_database_metadata(&self) -> &ServiceDatabaseMetadata { 290 &self.database 291 } 292 293 /// Returns the reopen identity with this binary's exact schema ceiling. 294 #[must_use] 295 pub fn database_identity(&self) -> ServiceDatabaseIdentity { 296 self.database_identity.clone() 297 } 298 299 /// Returns the normalized configuration digest. 300 #[must_use] 301 pub const fn configuration_digest(&self) -> MycNormalizedConfigDigest { 302 self.configuration 303 } 304 305 /// Returns the exact configured identity-role bindings. 306 #[must_use] 307 pub const fn expected_identities(&self) -> &MycExpectedIdentities { 308 &self.identities 309 } 310 311 /// Returns the exact governed policy versions. 312 #[must_use] 313 pub const fn policy_versions(&self) -> MycStatePolicyVersions { 314 self.policy_versions 315 } 316 317 pub(crate) const fn governance_rate_policy( 318 &self, 319 class: MycRateLimitClass, 320 ) -> MycRateLimitPolicy { 321 self.governance.rate_policy(class) 322 } 323 324 pub(crate) fn admits_rate_relay(&self, relay: &MycRateRelayId) -> bool { 325 self.governance.admits_relay(relay) 326 } 327 328 pub(crate) const fn governance_audit_retention_ms(&self) -> u64 { 329 self.governance.audit_retention_ms() 330 } 331 332 pub(crate) fn admits_connection_request( 333 &self, 334 request: &MycConnectionAdmissionRequest, 335 ) -> bool { 336 self.authorization.admits_connection_request(request) 337 } 338 339 pub(crate) fn admits_connection_operator_decision( 340 &self, 341 observed_at: MycConnectionTimeUnixMs, 342 decision: &MycConnectionOperatorDecision, 343 ) -> bool { 344 self.authorization 345 .admits_operator_decision(observed_at, decision) 346 } 347 348 pub(crate) fn admits_authorization_challenge_request( 349 &self, 350 request: &MycAuthorizationChallengeRequest, 351 ) -> bool { 352 self.authorization.admits_challenge_request(request) 353 } 354 355 pub(crate) fn authorization_challenge_is_current( 356 &self, 357 record: &MycAuthorizationChallengeRecord, 358 observed_at: MycConnectionTimeUnixMs, 359 ) -> bool { 360 self.authorization.challenge_is_current(record, observed_at) 361 } 362 363 pub(crate) const fn delivery_policies(&self) -> &MycDeliveryPolicies { 364 &self.delivery 365 } 366 367 pub(crate) const fn outbox_maximum(&self) -> usize { 368 self.delivery.outbox_maximum() 369 } 370 371 pub(crate) const fn discovery_policies(&self) -> Option<&MycDiscoveryPolicies> { 372 self.discovery.as_ref() 373 } 374 375 pub(crate) fn matches_runtime(&self, runtime: &MycRuntimeContext) -> bool { 376 ServiceSqlitePaths::from_runtime_context(runtime.context()) 377 .is_ok_and(|paths| paths == self.paths) 378 } 379 380 pub(crate) fn matches_configuration( 381 &self, 382 runtime: &MycRuntimeContext, 383 configuration: &MycConfigDocumentV1, 384 ) -> bool { 385 self.matches_runtime(runtime) 386 && require_profile_binding(runtime.profile(), configuration.profile()).is_ok() 387 && normalized_config_digest(configuration.profile(), configuration.normalized()) 388 .is_ok_and(|digest| digest == self.configuration) 389 } 390 } 391 392 impl fmt::Debug for MycStateMetadata { 393 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 394 formatter 395 .debug_struct("MycStateMetadata") 396 .field("database", &self.database) 397 .field("database_identity", &self.database_identity) 398 .field("configuration", &self.configuration) 399 .field("identities", &self.identities) 400 .field("governance", &"[redacted]") 401 .field("delivery", &"[redacted]") 402 .field("outbox_maximum", &self.delivery.outbox_maximum()) 403 .field("discovery", &self.discovery.as_ref().map(|_| "[redacted]")) 404 .field("policy_versions", &self.policy_versions) 405 .field("paths", &"[redacted]") 406 .finish() 407 } 408 } 409 410 /// Stable source-free class for invalid Myc state metadata. 411 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 412 pub enum MycStateMetadataErrorKind { 413 Profile, 414 Paths, 415 Configuration, 416 Identity, 417 Database, 418 Invariant, 419 } 420 421 /// Source-free Myc state-metadata construction failure. 422 #[derive(Clone, Copy, PartialEq, Eq)] 423 pub struct MycStateMetadataError { 424 kind: MycStateMetadataErrorKind, 425 } 426 427 impl MycStateMetadataError { 428 const fn new(kind: MycStateMetadataErrorKind) -> Self { 429 Self { kind } 430 } 431 432 #[must_use] 433 pub const fn kind(self) -> MycStateMetadataErrorKind { 434 self.kind 435 } 436 } 437 438 impl fmt::Display for MycStateMetadataError { 439 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 440 formatter.write_str(match self.kind { 441 MycStateMetadataErrorKind::Profile => "Myc configuration profile is inconsistent", 442 MycStateMetadataErrorKind::Paths => "Myc state metadata paths are invalid", 443 MycStateMetadataErrorKind::Configuration => { 444 "Myc normalized configuration identity is invalid" 445 } 446 MycStateMetadataErrorKind::Identity => "Myc expected identity binding is invalid", 447 MycStateMetadataErrorKind::Database => "Myc database metadata is invalid", 448 MycStateMetadataErrorKind::Invariant => "Myc metadata contract is invalid", 449 }) 450 } 451 } 452 453 impl fmt::Debug for MycStateMetadataError { 454 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 455 formatter 456 .debug_struct("MycStateMetadataError") 457 .field("kind", &self.kind) 458 .finish() 459 } 460 } 461 462 impl Error for MycStateMetadataError {} 463 464 fn require_profile_binding( 465 runtime: MycBootstrapProfileV1, 466 configuration: MycConfigProfile, 467 ) -> Result<(), MycStateMetadataError> { 468 let matches = match runtime { 469 MycBootstrapProfileV1::ServiceHost | MycBootstrapProfileV1::Interactive => { 470 configuration == MycConfigProfile::Production 471 } 472 MycBootstrapProfileV1::RepoLocal => configuration == MycConfigProfile::RepoLocal, 473 }; 474 matches 475 .then_some(()) 476 .ok_or_else(|| MycStateMetadataError::new(MycStateMetadataErrorKind::Profile)) 477 } 478 479 pub(crate) fn normalized_config_digest( 480 profile: MycConfigProfile, 481 normalized: &serde_json::Value, 482 ) -> Result<MycNormalizedConfigDigest, MycStateMetadataError> { 483 let bytes = serde_json::to_vec(normalized) 484 .map_err(|_| MycStateMetadataError::new(MycStateMetadataErrorKind::Configuration))?; 485 let length = u64::try_from(bytes.len()) 486 .map_err(|_| MycStateMetadataError::new(MycStateMetadataErrorKind::Configuration))?; 487 let mut hasher = Sha256::new(); 488 hasher.update(NORMALIZED_CONFIG_DIGEST_DOMAIN); 489 hasher.update([match profile { 490 MycConfigProfile::Production => 0, 491 MycConfigProfile::RepoLocal => 1, 492 }]); 493 hasher.update(length.to_be_bytes()); 494 hasher.update(bytes); 495 Ok(MycNormalizedConfigDigest(hasher.finalize().into())) 496 } 497 498 fn governance_policies( 499 normalized: &serde_json::Value, 500 ) -> Result<MycGovernancePolicies, MycStateMetadataError> { 501 let integer = |pointer: &str| { 502 normalized 503 .pointer(pointer) 504 .and_then(serde_json::Value::as_u64) 505 .ok_or_else(|| MycStateMetadataError::new(MycStateMetadataErrorKind::Invariant)) 506 }; 507 let policy = |class, name: &str| { 508 let prefix = format!("/rate_limits/{name}"); 509 MycRateLimitPolicy::new( 510 class, 511 integer(&format!("{prefix}/window_ms"))?, 512 u32::try_from(integer(&format!("{prefix}/max_attempts"))?) 513 .map_err(|_| MycStateMetadataError::new(MycStateMetadataErrorKind::Invariant))?, 514 integer(&format!("{prefix}/retention_ms"))?, 515 u32::try_from(integer(&format!("{prefix}/maximum_tracked_subjects"))?) 516 .map_err(|_| MycStateMetadataError::new(MycStateMetadataErrorKind::Invariant))?, 517 ) 518 .map_err(|_| MycStateMetadataError::new(MycStateMetadataErrorKind::Invariant)) 519 }; 520 let relays = normalized 521 .pointer("/relays") 522 .and_then(serde_json::Value::as_array) 523 .ok_or_else(|| MycStateMetadataError::new(MycStateMetadataErrorKind::Invariant))? 524 .iter() 525 .map(|relay| { 526 relay 527 .pointer("/id") 528 .and_then(serde_json::Value::as_str) 529 .ok_or_else(|| MycStateMetadataError::new(MycStateMetadataErrorKind::Invariant)) 530 .and_then(|id| { 531 MycRateRelayId::new(id).map_err(|_| { 532 MycStateMetadataError::new(MycStateMetadataErrorKind::Invariant) 533 }) 534 }) 535 }) 536 .collect::<Result<Vec<_>, _>>()? 537 .into_boxed_slice(); 538 MycGovernancePolicies::new( 539 policy( 540 MycRateLimitClass::ConnectionAdmission, 541 "connection_admission", 542 )?, 543 policy(MycRateLimitClass::ChallengeCreation, "challenge_creation")?, 544 policy( 545 MycRateLimitClass::ChallengeAuthorization, 546 "challenge_authorization", 547 )?, 548 integer("/policy/retention/audit_ms")?, 549 relays, 550 ) 551 .map_err(|_| MycStateMetadataError::new(MycStateMetadataErrorKind::Invariant)) 552 } 553 554 fn delivery_policies( 555 normalized: &serde_json::Value, 556 ) -> Result<MycDeliveryPolicies, MycStateMetadataError> { 557 let invalid = || MycStateMetadataError::new(MycStateMetadataErrorKind::Invariant); 558 let integer = |pointer: &str| { 559 normalized 560 .pointer(pointer) 561 .and_then(serde_json::Value::as_u64) 562 .ok_or_else(invalid) 563 }; 564 let mode = normalized 565 .pointer("/transport/delivery_policy/mode") 566 .and_then(serde_json::Value::as_str) 567 .and_then(MycDeliveryPolicyMode::parse) 568 .ok_or_else(invalid)?; 569 let configured_quorum = normalized 570 .pointer("/transport/delivery_policy/required_acknowledgements") 571 .map(|value| { 572 value 573 .as_u64() 574 .and_then(|number| u32::try_from(number).ok()) 575 .ok_or_else(invalid) 576 }) 577 .transpose()?; 578 let targets = normalized 579 .pointer("/relays") 580 .and_then(serde_json::Value::as_array) 581 .ok_or_else(invalid)? 582 .iter() 583 .filter(|relay| relay.pointer("/write").and_then(serde_json::Value::as_bool) == Some(true)) 584 .map(|relay| { 585 let id = relay 586 .pointer("/id") 587 .and_then(serde_json::Value::as_str) 588 .ok_or_else(invalid)?; 589 let required = relay 590 .pointer("/required") 591 .and_then(serde_json::Value::as_bool) 592 .ok_or_else(invalid)?; 593 let id = MycDeliveryRelayId::new(id).map_err(|_| invalid())?; 594 Ok((id, required)) 595 }) 596 .collect::<Result<Vec<_>, MycStateMetadataError>>()?; 597 MycDeliveryPolicies::new( 598 mode, 599 configured_quorum, 600 u32::try_from(integer("/transport/publish_retry/max_attempts")?).map_err(|_| invalid())?, 601 integer("/transport/publish_retry/initial_backoff_ms")?, 602 integer("/transport/publish_retry/maximum_backoff_ms")?, 603 integer("/transport/publish_retry/attempt_deadline_ms")?, 604 usize::try_from(integer("/resource_limits/queues/outbox")?).map_err(|_| invalid())?, 605 targets, 606 ) 607 .map_err(|_| invalid()) 608 } 609 610 pub(crate) fn expected_identities( 611 normalized: &serde_json::Value, 612 ) -> Result<MycExpectedIdentities, MycStateMetadataError> { 613 let identity = |pointer: &str| { 614 normalized 615 .pointer(pointer) 616 .and_then(serde_json::Value::as_str) 617 .ok_or_else(|| MycStateMetadataError::new(MycStateMetadataErrorKind::Identity)) 618 .and_then(MycExpectedPublicIdentity::from_hex) 619 }; 620 let discovery_enabled = normalized 621 .pointer("/identity/discovery/enabled") 622 .and_then(serde_json::Value::as_bool) 623 .ok_or_else(|| MycStateMetadataError::new(MycStateMetadataErrorKind::Identity))?; 624 Ok(MycExpectedIdentities { 625 transport: identity("/identity/transport/expected_public_key")?, 626 user: identity("/identity/user/expected_public_key")?, 627 discovery: if discovery_enabled { 628 Some(identity("/identity/discovery/binding/expected_public_key")?) 629 } else { 630 None 631 }, 632 }) 633 }