services_hardening_runtime_foundation.rs (18495B)
1 #![forbid(unsafe_code)] 2 #![cfg(any(target_os = "linux", target_os = "macos"))] 3 4 use std::{error::Error, fs, os::unix::fs::PermissionsExt, path::Path}; 5 6 use myc::{ 7 MycConfigDocumentV1, MycConfigProfile, MycEncryptedIdentityProvisioningMaterial, 8 MycProviderKind, MycProviderRole, MycRuntimeFoundationErrorKind, MycRuntimePrerequisite, 9 MycStateMetadata, RadrootsHostEnvironment, RadrootsPathResolver, RadrootsPlatform, 10 initialize_myc_state, open_myc_runtime_foundation, open_myc_state_read_write, 11 parse_myc_cli_v1_from, parse_myc_config_v1, provision_myc_encrypted_identity, 12 resolve_myc_runtime_context, resolve_myc_wrapping_credential, 13 }; 14 use nostr::{Keys, SecretKey}; 15 use radroots_service_sqlite::{MigrationAppliedAtUnixSeconds, MigrationBuildIdentity}; 16 use radroots_storage::event::SourceGeneration; 17 use serde_json::json; 18 19 const FOUNDATION_SOURCE: &str = include_str!("../src/runtime_foundation.rs"); 20 const LIB_SOURCE: &str = include_str!("../src/lib.rs"); 21 const CONTRACT_SOURCE: &str = 22 include_str!("../contracts/services_hardening/runtime_foundation.v1.json"); 23 const CONFIG_EXAMPLE: &str = include_str!("../contracts/services_hardening/config.v1.example.toml"); 24 25 const TRANSPORT_ENCRYPTED: &str = r#"[identity.transport] 26 provider = "encrypted_file" 27 envelope_path = "/var/lib/radroots/services/myc/primary/secrets/transport.identity.ncrypt" 28 credential_reference = "transport_wrapping_key" 29 expected_public_key = "4444444444444444444444444444444444444444444444444444444444444444""#; 30 31 const DISCOVERY_ENCRYPTED: &str = r#"[identity.discovery.binding] 32 provider = "encrypted_file" 33 envelope_path = "/var/lib/radroots/services/myc/primary/secrets/discovery.identity.ncrypt" 34 credential_reference = "discovery_wrapping_key" 35 expected_public_key = "3333333333333333333333333333333333333333333333333333333333333333""#; 36 37 fn runtime(root: &Path) -> myc::MycRuntimeContext { 38 let invocation = parse_myc_cli_v1_from([ 39 "myc", 40 "--profile", 41 "repo-local", 42 "--instance", 43 "primary", 44 "--repo-local-root", 45 root.to_str().expect("UTF-8 temporary root"), 46 "run", 47 ]) 48 .expect("valid test invocation"); 49 resolve_myc_runtime_context( 50 &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()), 51 &invocation, 52 ) 53 .expect("runtime context") 54 } 55 56 fn prepare_state_directory(runtime: &myc::MycRuntimeContext) { 57 let directory = runtime.context().paths().state(); 58 fs::create_dir_all(directory).expect("state directory"); 59 fs::set_permissions(directory, fs::Permissions::from_mode(0o700)).expect("state mode"); 60 } 61 62 fn local_transport_block(root: &Path) -> String { 63 format!( 64 r#"[identity.transport] 65 provider = "local_signer" 66 socket_path = "{}" 67 request_deadline_ms = 15000 68 request_max_bytes = 65536 69 response_max_bytes = 1048576 70 concurrency = 32 71 expected_public_key = "4444444444444444444444444444444444444444444444444444444444444444""#, 72 root.join("transport-signer.sock").display() 73 ) 74 } 75 76 fn local_discovery_block(root: &Path) -> String { 77 format!( 78 r#"[identity.discovery.binding] 79 provider = "local_signer" 80 socket_path = "{}" 81 request_deadline_ms = 15000 82 request_max_bytes = 65536 83 response_max_bytes = 1048576 84 concurrency = 32 85 expected_public_key = "3333333333333333333333333333333333333333333333333333333333333333""#, 86 root.join("discovery-signer.sock").display() 87 ) 88 } 89 90 fn all_local_source(root: &Path) -> String { 91 let transport = local_transport_block(root); 92 let discovery = local_discovery_block(root); 93 let source = CONFIG_EXAMPLE 94 .replacen(TRANSPORT_ENCRYPTED, &transport, 1) 95 .replacen(DISCOVERY_ENCRYPTED, &discovery, 1); 96 assert!(!source.contains(TRANSPORT_ENCRYPTED)); 97 assert!(!source.contains(DISCOVERY_ENCRYPTED)); 98 source 99 } 100 101 fn all_local_configuration(root: &Path) -> MycConfigDocumentV1 { 102 parse_myc_config_v1( 103 all_local_source(root).as_bytes(), 104 MycConfigProfile::RepoLocal, 105 ) 106 .expect("all-local configuration") 107 } 108 109 fn example_configuration() -> MycConfigDocumentV1 { 110 parse_myc_config_v1(CONFIG_EXAMPLE.as_bytes(), MycConfigProfile::RepoLocal) 111 .expect("example configuration") 112 } 113 114 fn metadata( 115 runtime: &myc::MycRuntimeContext, 116 configuration: &MycConfigDocumentV1, 117 ) -> MycStateMetadata { 118 MycStateMetadata::new( 119 runtime, 120 configuration, 121 SourceGeneration::new([0x5a; 32]).expect("generation"), 122 1_725_000_000_000, 123 ) 124 .expect("metadata") 125 } 126 127 fn migration_evidence() -> (MigrationAppliedAtUnixSeconds, MigrationBuildIdentity) { 128 let applied_at = MigrationAppliedAtUnixSeconds::new(1_725_000_000).expect("migration time"); 129 let build = MigrationBuildIdentity::new( 130 env!("CARGO_PKG_VERSION"), 131 "1111111111111111111111111111111111111111", 132 "053d0c750bf9cd683c6ea37cefe7e79617ba629f", 133 "rustc-test", 134 "test-target", 135 "service-host", 136 1, 137 myc::MYC_STATE_SCHEMA_VERSION, 138 1, 139 1, 140 1, 141 ) 142 .expect("build identity"); 143 (applied_at, build) 144 } 145 146 #[tokio::test] 147 async fn foundation_owns_existing_state_and_never_claims_unproven_readiness() { 148 let directory = tempfile::tempdir().expect("temporary root"); 149 let runtime = runtime(directory.path()); 150 prepare_state_directory(&runtime); 151 let configuration = all_local_configuration(directory.path()); 152 let state_metadata = metadata(&runtime, &configuration); 153 let (applied_at, build) = migration_evidence(); 154 initialize_myc_state(&runtime, &state_metadata, applied_at, &build) 155 .await 156 .expect("state initialization"); 157 158 let foundation = open_myc_runtime_foundation( 159 runtime.clone(), 160 configuration, 161 state_metadata.clone(), 162 applied_at, 163 &build, 164 ) 165 .await 166 .expect("existing-state foundation"); 167 for role in [ 168 MycProviderRole::Transport, 169 MycProviderRole::User, 170 MycProviderRole::Discovery, 171 ] { 172 assert_eq!( 173 foundation.provider_kind(role), 174 Some(MycProviderKind::LocalSigner) 175 ); 176 } 177 assert!(!foundation.readiness().is_ready()); 178 assert_eq!( 179 foundation.readiness().required(), 180 [ 181 MycRuntimePrerequisite::ExistingState, 182 MycRuntimePrerequisite::TransportProvider, 183 MycRuntimePrerequisite::UserProvider, 184 MycRuntimePrerequisite::DiscoveryProvider, 185 MycRuntimePrerequisite::OutboxRecovery, 186 MycRuntimePrerequisite::RequiredRelayConnectivity, 187 MycRuntimePrerequisite::RequiredRelaySubscription, 188 MycRuntimePrerequisite::AdminListener, 189 ] 190 ); 191 assert_eq!( 192 foundation.readiness().satisfied(), 193 [MycRuntimePrerequisite::ExistingState] 194 ); 195 assert_eq!( 196 foundation 197 .readiness() 198 .reasons() 199 .iter() 200 .map(|reason| reason.as_str()) 201 .collect::<Vec<_>>(), 202 [ 203 "admin_listener_failed", 204 "outbox_invariant_failed", 205 "required_relay_unavailable", 206 "signer_provider_unavailable", 207 "subscriber_not_active", 208 ] 209 ); 210 211 let rendered = format!("{foundation:?}"); 212 assert!(!rendered.contains(directory.path().to_string_lossy().as_ref())); 213 assert!(!rendered.contains("transport-signer.sock")); 214 assert!(!rendered.contains("4444444444444444")); 215 216 let contended = open_myc_state_read_write(&runtime, &state_metadata, applied_at, &build) 217 .await 218 .expect_err("foundation retains writer authority"); 219 assert_eq!(contended.kind(), myc::MycStateHostErrorKind::ReadWriteOpen); 220 221 foundation.shutdown().await.expect("joined shutdown"); 222 let reopened = open_myc_state_read_write(&runtime, &state_metadata, applied_at, &build) 223 .await 224 .expect("authority released after joined shutdown"); 225 reopened.close().await.expect("reopened state close"); 226 } 227 228 #[tokio::test] 229 async fn missing_state_and_configuration_mismatch_fail_without_mutation_or_lock_leak() { 230 let missing_directory = tempfile::tempdir().expect("missing temporary root"); 231 let missing_runtime = runtime(missing_directory.path()); 232 prepare_state_directory(&missing_runtime); 233 let missing_configuration = all_local_configuration(missing_directory.path()); 234 let missing_metadata = metadata(&missing_runtime, &missing_configuration); 235 let (applied_at, build) = migration_evidence(); 236 let missing = open_myc_runtime_foundation( 237 missing_runtime.clone(), 238 missing_configuration, 239 missing_metadata, 240 applied_at, 241 &build, 242 ) 243 .await 244 .expect_err("run never initializes missing state"); 245 assert_eq!(missing.kind(), MycRuntimeFoundationErrorKind::StateOpen); 246 assert!(!missing_runtime.artifacts().state_database().exists()); 247 248 let directory = tempfile::tempdir().expect("mismatch temporary root"); 249 let runtime = runtime(directory.path()); 250 prepare_state_directory(&runtime); 251 let original = all_local_configuration(directory.path()); 252 let state_metadata = metadata(&runtime, &original); 253 initialize_myc_state(&runtime, &state_metadata, applied_at, &build) 254 .await 255 .expect("state initialization"); 256 let changed_source = CONFIG_EXAMPLE.replacen("level = \"info\"", "level = \"debug\"", 1); 257 let changed = parse_myc_config_v1(changed_source.as_bytes(), MycConfigProfile::RepoLocal) 258 .expect("changed configuration"); 259 let mismatch = open_myc_runtime_foundation( 260 runtime.clone(), 261 changed, 262 state_metadata.clone(), 263 applied_at, 264 &build, 265 ) 266 .await 267 .expect_err("configuration digest mismatch"); 268 assert_eq!( 269 mismatch.kind(), 270 MycRuntimeFoundationErrorKind::InvalidBinding 271 ); 272 assert!(Error::source(&mismatch).is_none()); 273 274 let reopened = open_myc_state_read_write(&runtime, &state_metadata, applied_at, &build) 275 .await 276 .expect("mismatch fails before writer acquisition"); 277 reopened.close().await.expect("reopened state close"); 278 } 279 280 #[tokio::test] 281 async fn joined_encrypted_provider_failure_closes_the_already_open_state() { 282 let directory = tempfile::tempdir().expect("temporary root"); 283 let runtime = runtime(directory.path()); 284 prepare_state_directory(&runtime); 285 let configuration = example_configuration(); 286 let state_metadata = metadata(&runtime, &configuration); 287 let (applied_at, build) = migration_evidence(); 288 initialize_myc_state(&runtime, &state_metadata, applied_at, &build) 289 .await 290 .expect("state initialization"); 291 292 let failure = open_myc_runtime_foundation( 293 runtime.clone(), 294 configuration, 295 state_metadata.clone(), 296 applied_at, 297 &build, 298 ) 299 .await 300 .expect_err("missing credential artifacts fail provider startup"); 301 assert_eq!(failure.kind(), MycRuntimeFoundationErrorKind::Provider); 302 assert!(Error::source(&failure).is_none()); 303 304 let reopened = open_myc_state_read_write(&runtime, &state_metadata, applied_at, &build) 305 .await 306 .expect("failed provider startup closes state"); 307 reopened.close().await.expect("reopened state close"); 308 } 309 310 #[tokio::test] 311 async fn joined_encrypted_provider_success_is_retained_as_proven_startup_evidence() { 312 let directory = tempfile::tempdir().expect("temporary root"); 313 let runtime = runtime(directory.path()); 314 prepare_state_directory(&runtime); 315 316 let identity_secret = [1_u8; 32]; 317 let public_key = Keys::new(SecretKey::from_slice(&identity_secret).expect("identity secret")) 318 .public_key() 319 .to_hex(); 320 let envelope_parent = directory.path().join("envelopes"); 321 fs::create_dir(&envelope_parent).expect("envelope parent"); 322 fs::set_permissions(&envelope_parent, fs::Permissions::from_mode(0o700)) 323 .expect("envelope parent mode"); 324 let envelope_path = envelope_parent.join("transport.identity.ncrypt"); 325 let transport_encrypted = format!( 326 r#"[identity.transport] 327 provider = "encrypted_file" 328 envelope_path = "{}" 329 credential_reference = "transport_wrapping_key" 330 expected_public_key = "{}""#, 331 envelope_path.display(), 332 public_key 333 ); 334 let source = all_local_source(directory.path()).replacen( 335 &local_transport_block(directory.path()), 336 &transport_encrypted, 337 1, 338 ); 339 let configuration = parse_myc_config_v1(source.as_bytes(), MycConfigProfile::RepoLocal) 340 .expect("mixed provider configuration"); 341 let binding = configuration 342 .provider_contract() 343 .binding(MycProviderRole::Transport) 344 .expect("transport binding"); 345 346 let secrets = runtime.context().paths().secrets(); 347 fs::create_dir_all(secrets).expect("secrets directory"); 348 fs::set_permissions(secrets, fs::Permissions::from_mode(0o700)) 349 .expect("secrets directory mode"); 350 let credential_path = secrets.join("transport_wrapping_key"); 351 fs::write(&credential_path, [9_u8; 32]).expect("offline credential fixture"); 352 fs::set_permissions(&credential_path, fs::Permissions::from_mode(0o600)) 353 .expect("credential mode"); 354 let credential = 355 resolve_myc_wrapping_credential(&runtime, binding).expect("credential resolution"); 356 let material = MycEncryptedIdentityProvisioningMaterial::new( 357 identity_secret, 358 [2_u8; 32], 359 [3_u8; 24], 360 [4_u8; 24], 361 ) 362 .expect("provisioning material"); 363 provision_myc_encrypted_identity(binding, &credential, material) 364 .expect("offline envelope provisioning"); 365 366 let state_metadata = metadata(&runtime, &configuration); 367 let (applied_at, build) = migration_evidence(); 368 initialize_myc_state(&runtime, &state_metadata, applied_at, &build) 369 .await 370 .expect("state initialization"); 371 let foundation = 372 open_myc_runtime_foundation(runtime, configuration, state_metadata, applied_at, &build) 373 .await 374 .expect("joined encrypted-provider startup"); 375 assert_eq!( 376 foundation.provider_kind(MycProviderRole::Transport), 377 Some(MycProviderKind::EncryptedFile) 378 ); 379 assert!( 380 foundation 381 .readiness() 382 .satisfied() 383 .contains(&MycRuntimePrerequisite::TransportProvider) 384 ); 385 assert!(!foundation.readiness().is_ready()); 386 foundation.shutdown().await.expect("joined shutdown"); 387 } 388 389 #[test] 390 fn machine_contract_and_source_keep_the_foundation_sealed_and_deferred() { 391 let contract: serde_json::Value = 392 serde_json::from_str(CONTRACT_SOURCE).expect("runtime-foundation contract"); 393 assert_eq!( 394 contract, 395 json!({ 396 "schema": "radroots.myc.runtime-foundation", 397 "schema_version": 1, 398 "contract_version": 1, 399 "state_open": { 400 "mode": "read_write_existing", 401 "initialize_if_missing": false, 402 "raw_sqlite_authority_exposed": false 403 }, 404 "provider_startup": { 405 "encrypted_file": "supervised_joined_one_shot", 406 "local_signer": "constructed_without_io_pending_handshake", 407 "database_transaction_held": false, 408 "detached_tasks": false, 409 "protected_values_exposed": false 410 }, 411 "readiness_prerequisites": [ 412 { "id": "existing_state", "condition": "always", "reason": "database_schema_mismatch" }, 413 { "id": "transport_provider", "condition": "always", "reason": "signer_provider_unavailable" }, 414 { "id": "user_provider", "condition": "always", "reason": "signer_provider_unavailable" }, 415 { "id": "discovery_provider", "condition": "discovery_enabled", "reason": "signer_provider_unavailable" }, 416 { "id": "outbox_recovery", "condition": "always", "reason": "outbox_invariant_failed" }, 417 { "id": "required_relay_connectivity", "condition": "required_relay_present", "reason": "required_relay_unavailable" }, 418 { "id": "required_relay_subscription", "condition": "required_read_relay_present", "reason": "subscriber_not_active" }, 419 { "id": "admin_listener", "condition": "always", "reason": "admin_listener_failed" }, 420 { "id": "operations_listener", "condition": "operations_enabled", "reason": "operations_listener_failed" } 421 ], 422 "initial_satisfaction": { 423 "existing_state": "after_exact_existing_open", 424 "encrypted_file_provider": "after_joined_identity_verification", 425 "local_signer_provider": "not_before_verified_describe_handshake", 426 "remaining_prerequisites": "later_owning_rcld" 427 }, 428 "task_ownership": { 429 "shared_supervisor": "radroots_service_host::TaskSupervisor", 430 "startup_tasks": "one_shot", 431 "lifetime_task": "critical_until_cancellation", 432 "task_handles_exposed": false, 433 "library_runtime_creation": false, 434 "signal_installation": false, 435 "process_exit": false 436 }, 437 "deferred": [ 438 "provider_handshake", 439 "outbox_recovery", 440 "relay_connectivity", 441 "relay_subscription", 442 "admin_listener", 443 "operations_listener", 444 "cached_status", 445 "signals", 446 "final_supervised_task_graph" 447 ] 448 }) 449 ); 450 451 assert!(LIB_SOURCE.contains("mod runtime_foundation;")); 452 assert!(!LIB_SOURCE.contains("pub mod runtime_foundation;")); 453 for required in [ 454 "TaskSupervisor::new()", 455 "TaskClassification::OneShot", 456 "thread.join()", 457 "open_myc_state_read_write", 458 ] { 459 assert!( 460 FOUNDATION_SOURCE.contains(required), 461 "missing required foundation boundary `{required}`" 462 ); 463 } 464 for forbidden in [ 465 "tokio::spawn", 466 "spawn_blocking", 467 "JoinHandle", 468 "Runtime::new", 469 "process::exit", 470 "pub fn pool", 471 "pub fn connection", 472 "AdminServer::", 473 "TcpListener", 474 ] { 475 assert!( 476 !FOUNDATION_SOURCE.contains(forbidden), 477 "found deferred or escaping authority `{forbidden}`" 478 ); 479 } 480 }