services_hardening_state_repository.rs (12212B)
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 MYC_STATE_SCHEMA_VERSION, MycConfigProfile, MycStateHostErrorKind, MycStateMetadata, 8 MycStateRepositoryErrorKind, RadrootsHostEnvironment, RadrootsPathResolver, RadrootsPlatform, 9 initialize_myc_state, open_myc_state_inspection, open_myc_state_read_write, 10 parse_myc_cli_v1_from, parse_myc_config_v1, resolve_myc_runtime_context, 11 }; 12 use radroots_service_sqlite::{MigrationAppliedAtUnixSeconds, MigrationBuildIdentity}; 13 use radroots_storage::event::SourceGeneration; 14 use sqlx::{ConnectOptions, Connection, Row, sqlite::SqliteConnectOptions}; 15 16 const CONFIG_EXAMPLE: &[u8] = 17 include_bytes!("../contracts/services_hardening/config.v1.example.toml"); 18 const LIB_SOURCE: &str = include_str!("../src/lib.rs"); 19 const HOST_SOURCE: &str = include_str!("../src/state_host.rs"); 20 const REPOSITORY_SOURCE: &str = include_str!("../src/state_repository.rs"); 21 22 fn runtime(root: &Path) -> myc::MycRuntimeContext { 23 let root = root.to_str().expect("UTF-8 temporary root"); 24 let invocation = parse_myc_cli_v1_from([ 25 "myc", 26 "--profile", 27 "repo-local", 28 "--instance", 29 "primary", 30 "--repo-local-root", 31 root, 32 "run", 33 ]) 34 .expect("valid test invocation"); 35 resolve_myc_runtime_context( 36 &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()), 37 &invocation, 38 ) 39 .expect("runtime context") 40 } 41 42 fn prepare_state_directory(runtime: &myc::MycRuntimeContext) { 43 let directory = runtime.context().paths().state(); 44 fs::create_dir_all(directory).expect("state directory"); 45 fs::set_permissions(directory, fs::Permissions::from_mode(0o700)).expect("state mode"); 46 } 47 48 fn metadata(runtime: &myc::MycRuntimeContext, source: &[u8]) -> MycStateMetadata { 49 let configuration = 50 parse_myc_config_v1(source, MycConfigProfile::RepoLocal).expect("configuration"); 51 MycStateMetadata::new( 52 runtime, 53 &configuration, 54 SourceGeneration::new([0x5a; 32]).expect("generation"), 55 1_725_000_000_000, 56 ) 57 .expect("metadata") 58 } 59 60 fn migration_evidence() -> (MigrationAppliedAtUnixSeconds, MigrationBuildIdentity) { 61 let applied_at = MigrationAppliedAtUnixSeconds::new(1_725_000_000).expect("migration time"); 62 let build = MigrationBuildIdentity::new( 63 env!("CARGO_PKG_VERSION"), 64 "1111111111111111111111111111111111111111", 65 "053d0c750bf9cd683c6ea37cefe7e79617ba629f", 66 "rustc-test", 67 "test-target", 68 "service-host", 69 1, 70 MYC_STATE_SCHEMA_VERSION, 71 1, 72 1, 73 1, 74 ) 75 .expect("build identity"); 76 (applied_at, build) 77 } 78 79 #[tokio::test] 80 async fn initialization_migrates_and_binds_exact_metadata_before_inspection() { 81 let directory = tempfile::tempdir().expect("temporary root"); 82 let runtime = runtime(directory.path()); 83 prepare_state_directory(&runtime); 84 let metadata = metadata(&runtime, CONFIG_EXAMPLE); 85 let (applied_at, build) = migration_evidence(); 86 87 initialize_myc_state(&runtime, &metadata, applied_at, &build) 88 .await 89 .expect("initialized current state"); 90 let inspection = open_myc_state_inspection(&runtime, &metadata) 91 .await 92 .expect("current inspection"); 93 inspection 94 .repository() 95 .verify_binding() 96 .await 97 .expect("typed repository verification"); 98 assert_eq!( 99 format!("{:?}", inspection.repository()), 100 "MycStateRepository { state: \"[sealed]\" }" 101 ); 102 inspection.close().await.expect("inspection close"); 103 104 let options = SqliteConnectOptions::new() 105 .filename(runtime.artifacts().state_database()) 106 .create_if_missing(false) 107 .read_only(true) 108 .disable_statement_logging(); 109 let mut connection = sqlx::SqliteConnection::connect_with(&options) 110 .await 111 .expect("test inspection connection"); 112 let row = sqlx::query( 113 "SELECT state_schema_version FROM radroots_service_metadata WHERE singleton = 1", 114 ) 115 .fetch_one(&mut connection) 116 .await 117 .expect("shared metadata row"); 118 assert_eq!(row.get::<i64, _>(0), i64::from(MYC_STATE_SCHEMA_VERSION)); 119 let migrations = sqlx::query("SELECT version, name FROM schema_migrations ORDER BY version") 120 .fetch_all(&mut connection) 121 .await 122 .expect("migration rows"); 123 assert_eq!(migrations.len(), 11); 124 assert_eq!(migrations[0].get::<i64, _>(0), 2); 125 assert_eq!( 126 migrations[0].get::<String, _>(1), 127 "create_myc_state_metadata" 128 ); 129 assert_eq!(migrations[1].get::<i64, _>(0), 3); 130 assert_eq!( 131 migrations[1].get::<String, _>(1), 132 "create_nip46_request_admission" 133 ); 134 assert_eq!(migrations[2].get::<i64, _>(0), 4); 135 assert_eq!( 136 migrations[2].get::<String, _>(1), 137 "create_connection_authorization_state" 138 ); 139 assert_eq!(migrations[3].get::<i64, _>(0), 5); 140 assert_eq!( 141 migrations[3].get::<String, _>(1), 142 "create_bounded_governance_state" 143 ); 144 assert_eq!(migrations[4].get::<i64, _>(0), 6); 145 assert_eq!( 146 migrations[4].get::<String, _>(1), 147 "create_delivery_evidence_state" 148 ); 149 assert_eq!(migrations[5].get::<i64, _>(0), 7); 150 assert_eq!( 151 migrations[5].get::<String, _>(1), 152 "create_discovery_desired_state" 153 ); 154 assert_eq!(migrations[6].get::<i64, _>(0), 8); 155 assert_eq!( 156 migrations[6].get::<String, _>(1), 157 "create_nip46_operation_completion" 158 ); 159 assert_eq!(migrations[7].get::<i64, _>(0), 9); 160 assert_eq!( 161 migrations[7].get::<String, _>(1), 162 "create_nip46_atomic_response" 163 ); 164 assert_eq!(migrations[8].get::<i64, _>(0), 10); 165 assert_eq!( 166 migrations[8].get::<String, _>(1), 167 "create_configuration_binding_history" 168 ); 169 assert_eq!(migrations[9].get::<i64, _>(0), 11); 170 assert_eq!( 171 migrations[9].get::<String, _>(1), 172 "create_admin_operation_journal" 173 ); 174 assert_eq!(migrations[10].get::<i64, _>(0), 12); 175 assert_eq!( 176 migrations[10].get::<String, _>(1), 177 "create_nip46_pending_response_authority" 178 ); 179 let binding = sqlx::query( 180 "SELECT normalized_config_sha256, transport_public_key, user_public_key, \ 181 discovery_public_key, config_contract_version, state_contract_version, \ 182 operator_contract_version, status_contract_version \ 183 FROM myc_state_metadata WHERE singleton = 1", 184 ) 185 .fetch_one(&mut connection) 186 .await 187 .expect("Myc metadata row"); 188 assert_eq!( 189 binding.get::<Vec<u8>, _>(0), 190 metadata.configuration_digest().as_bytes() 191 ); 192 assert_eq!( 193 binding.get::<String, _>(1), 194 metadata.expected_identities().transport().as_hex() 195 ); 196 assert_eq!( 197 binding.get::<String, _>(2), 198 metadata.expected_identities().user().as_hex() 199 ); 200 assert_eq!( 201 binding.get::<String, _>(3), 202 metadata 203 .expected_identities() 204 .discovery() 205 .expect("discovery identity") 206 .as_hex() 207 ); 208 assert_eq!(binding.get::<i64, _>(4), 1); 209 assert_eq!( 210 binding.get::<i64, _>(5), 211 i64::from(MYC_STATE_SCHEMA_VERSION) 212 ); 213 assert_eq!(binding.get::<i64, _>(6), 1); 214 assert_eq!(binding.get::<i64, _>(7), 1); 215 connection.close().await.expect("test connection close"); 216 } 217 218 #[tokio::test] 219 async fn exact_binding_is_idempotent_and_conflicting_configuration_fails_closed() { 220 let directory = tempfile::tempdir().expect("temporary root"); 221 let runtime = runtime(directory.path()); 222 prepare_state_directory(&runtime); 223 let expected = metadata(&runtime, CONFIG_EXAMPLE); 224 let (applied_at, build) = migration_evidence(); 225 initialize_myc_state(&runtime, &expected, applied_at, &build) 226 .await 227 .expect("initialization"); 228 229 let writer = open_myc_state_read_write(&runtime, &expected, applied_at, &build) 230 .await 231 .expect("idempotent exact open"); 232 writer 233 .repository() 234 .verify_binding() 235 .await 236 .expect("exact binding"); 237 writer.close().await.expect("writer close"); 238 239 let changed_source = String::from_utf8(CONFIG_EXAMPLE.to_vec()) 240 .expect("UTF-8 config") 241 .replace("level = \"info\"", "level = \"warn\""); 242 let changed = metadata(&runtime, changed_source.as_bytes()); 243 let error = open_myc_state_read_write(&runtime, &changed, applied_at, &build) 244 .await 245 .expect_err("changed normalized binding"); 246 assert_eq!(error.kind(), MycStateHostErrorKind::Repository); 247 248 let inspection = open_myc_state_inspection(&runtime, &expected) 249 .await 250 .expect("original binding remains authoritative"); 251 inspection.close().await.expect("inspection close"); 252 } 253 254 #[tokio::test] 255 async fn database_guards_reject_metadata_update_and_delete() { 256 let directory = tempfile::tempdir().expect("temporary root"); 257 let runtime = runtime(directory.path()); 258 prepare_state_directory(&runtime); 259 let metadata = metadata(&runtime, CONFIG_EXAMPLE); 260 let (applied_at, build) = migration_evidence(); 261 initialize_myc_state(&runtime, &metadata, applied_at, &build) 262 .await 263 .expect("initialization"); 264 265 let options = SqliteConnectOptions::new() 266 .filename(runtime.artifacts().state_database()) 267 .create_if_missing(false) 268 .disable_statement_logging(); 269 let mut connection = sqlx::SqliteConnection::connect_with(&options) 270 .await 271 .expect("test connection"); 272 assert!( 273 sqlx::query("UPDATE myc_state_metadata SET status_contract_version = 2") 274 .execute(&mut connection) 275 .await 276 .is_err() 277 ); 278 assert!( 279 sqlx::query("DELETE FROM myc_state_metadata") 280 .execute(&mut connection) 281 .await 282 .is_err() 283 ); 284 connection.close().await.expect("test connection close"); 285 } 286 287 #[tokio::test] 288 async fn repository_boundary_is_sealed_typed_redacted_and_network_free() { 289 assert!(LIB_SOURCE.contains("mod state_repository;")); 290 assert!(!LIB_SOURCE.contains("pub mod state_repository;")); 291 assert!(HOST_SOURCE.contains("pub const fn repository(&self) -> MycStateRepository<'_>")); 292 assert!(REPOSITORY_SOURCE.contains(".transaction(move |transaction|")); 293 assert!(REPOSITORY_SOURCE.contains("INSERT INTO myc_state_metadata")); 294 for forbidden in [ 295 "SqliteConnection", 296 "SqlitePool", 297 "PoolConnection", 298 "BEGIN ", 299 "COMMIT", 300 "ROLLBACK", 301 "MycProvider", 302 "provider_credential", 303 "provider_envelope", 304 "relay", 305 "reqwest", 306 "nostr::", 307 "std::fs", 308 "std::path", 309 "std::env", 310 "std::time", 311 ] { 312 assert!( 313 !REPOSITORY_SOURCE.contains(forbidden), 314 "found forbidden repository authority `{forbidden}`" 315 ); 316 } 317 318 let kind = myc::MycStateRepositoryErrorKind::Binding; 319 assert_eq!(kind.code(), "state_repository_binding_invalid"); 320 let rendered = format!("{kind:?}"); 321 assert!(!rendered.contains("state.sqlite")); 322 323 let directory = tempfile::tempdir().expect("temporary root"); 324 let context = runtime(directory.path()); 325 prepare_state_directory(&context); 326 let metadata = metadata(&context, CONFIG_EXAMPLE); 327 let (applied_at, build) = migration_evidence(); 328 initialize_myc_state(&context, &metadata, applied_at, &build) 329 .await 330 .expect("initialization"); 331 let inspection = open_myc_state_inspection(&context, &metadata) 332 .await 333 .expect("inspection"); 334 inspection.close().await.expect("inspection close"); 335 let error = inspection 336 .repository() 337 .verify_binding() 338 .await 339 .expect_err("closed host rejects repository admission"); 340 assert_eq!(error.kind(), MycStateRepositoryErrorKind::Transaction); 341 assert!(Error::source(&error).is_none()); 342 assert!(!format!("{error} {error:?}").contains("secret")); 343 }