services_hardening_signer_request_state.rs (25064B)
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_NIP46_CANONICAL_REQUEST_MAX_BYTES, MYC_NIP46_REQUEST_ID_MAX_UTF8_BYTES, 8 MYC_STATE_SCHEMA_VERSION, MycConfigProfile, MycNip46ClientPublicKey, MycNip46EventId, 9 MycNip46RequestId, MycRequestReceivedAtUnixMs, MycSignerOperationNonce, MycSignerRequest, 10 MycSignerRequestAdmission, MycSignerRequestDigest, MycSignerRequestErrorKind, 11 MycSignerRequestMethod, MycStateMetadata, RadrootsHostEnvironment, RadrootsPathResolver, 12 RadrootsPlatform, initialize_myc_state, open_myc_state_read_write, parse_myc_cli_v1_from, 13 parse_myc_config_v1, resolve_myc_runtime_context, 14 }; 15 use radroots_service_sqlite::{MigrationAppliedAtUnixSeconds, MigrationBuildIdentity}; 16 use radroots_storage::event::SourceGeneration; 17 use sqlx::{ConnectOptions, Connection, sqlite::SqliteConnectOptions}; 18 19 const CONFIG_EXAMPLE: &[u8] = 20 include_bytes!("../contracts/services_hardening/config.v1.example.toml"); 21 const REQUEST_SOURCE: &str = include_str!("../src/state_request.rs"); 22 const CATALOG_SOURCE: &str = include_str!("../src/state_catalog.rs"); 23 const CLIENT_PUBLIC_KEY: &str = "2222222222222222222222222222222222222222222222222222222222222222"; 24 const REQUEST_BYTES: &[u8] = b"{\"id\":\"request-01\",\"method\":\"ping\",\"params\":[]}"; 25 26 fn runtime(root: &Path) -> myc::MycRuntimeContext { 27 let root = root.to_str().expect("UTF-8 temporary root"); 28 let invocation = parse_myc_cli_v1_from([ 29 "myc", 30 "--profile", 31 "repo-local", 32 "--instance", 33 "primary", 34 "--repo-local-root", 35 root, 36 "run", 37 ]) 38 .expect("valid test invocation"); 39 resolve_myc_runtime_context( 40 &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()), 41 &invocation, 42 ) 43 .expect("runtime context") 44 } 45 46 fn prepare_state_directory(runtime: &myc::MycRuntimeContext) { 47 let directory = runtime.context().paths().state(); 48 fs::create_dir_all(directory).expect("state directory"); 49 fs::set_permissions(directory, fs::Permissions::from_mode(0o700)).expect("state mode"); 50 } 51 52 fn metadata(runtime: &myc::MycRuntimeContext) -> MycStateMetadata { 53 let configuration = 54 parse_myc_config_v1(CONFIG_EXAMPLE, MycConfigProfile::RepoLocal).expect("configuration"); 55 MycStateMetadata::new( 56 runtime, 57 &configuration, 58 SourceGeneration::new([0x5a; 32]).expect("generation"), 59 1_725_000_000_000, 60 ) 61 .expect("metadata") 62 } 63 64 fn migration_evidence() -> (MigrationAppliedAtUnixSeconds, MigrationBuildIdentity) { 65 let applied_at = MigrationAppliedAtUnixSeconds::new(1_725_000_000).expect("migration time"); 66 let build = MigrationBuildIdentity::new( 67 env!("CARGO_PKG_VERSION"), 68 "1111111111111111111111111111111111111111", 69 "053d0c750bf9cd683c6ea37cefe7e79617ba629f", 70 "rustc-test", 71 "test-target", 72 "service-host", 73 1, 74 MYC_STATE_SCHEMA_VERSION, 75 1, 76 1, 77 1, 78 ) 79 .expect("build identity"); 80 (applied_at, build) 81 } 82 83 fn request( 84 request_id: &str, 85 event_byte: u8, 86 method: MycSignerRequestMethod, 87 bytes: &[u8], 88 nonce_byte: u8, 89 received_at: u64, 90 ) -> MycSignerRequest { 91 MycSignerRequest::new( 92 MycNip46ClientPublicKey::new(CLIENT_PUBLIC_KEY).expect("client identity"), 93 MycNip46RequestId::new(request_id).expect("request ID"), 94 MycNip46EventId::from_bytes([event_byte; 32]), 95 method, 96 MycSignerRequestDigest::for_canonical_request(bytes).expect("request digest"), 97 MycSignerOperationNonce::from_injected_entropy([nonce_byte; 32]), 98 MycRequestReceivedAtUnixMs::new(received_at).expect("received time"), 99 ) 100 } 101 102 fn hex(bytes: &[u8]) -> String { 103 bytes.iter().map(|byte| format!("{byte:02x}")).collect() 104 } 105 106 #[test] 107 fn request_inputs_ids_methods_and_diagnostics_are_closed_bounded_and_stable() { 108 let maximum_id = "a".repeat(MYC_NIP46_REQUEST_ID_MAX_UTF8_BYTES); 109 assert!(MycNip46RequestId::new(&maximum_id).is_ok()); 110 for invalid in [ 111 "", 112 " request", 113 "request ", 114 "request\n", 115 &"a".repeat(MYC_NIP46_REQUEST_ID_MAX_UTF8_BYTES + 1), 116 &"x".repeat(1024 * 1024), 117 ] { 118 assert_eq!( 119 MycNip46RequestId::new(invalid) 120 .expect_err("invalid request ID") 121 .kind(), 122 MycSignerRequestErrorKind::InvalidRequestId 123 ); 124 } 125 126 assert!(MycNip46ClientPublicKey::new(CLIENT_PUBLIC_KEY).is_ok()); 127 for invalid in [ 128 "22", 129 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", 130 "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", 131 ] { 132 assert_eq!( 133 MycNip46ClientPublicKey::new(invalid) 134 .expect_err("invalid client identity") 135 .kind(), 136 MycSignerRequestErrorKind::InvalidClientIdentity 137 ); 138 } 139 140 let maximum_request = vec![b'x'; MYC_NIP46_CANONICAL_REQUEST_MAX_BYTES]; 141 assert!(MycSignerRequestDigest::for_canonical_request(&maximum_request).is_ok()); 142 assert_eq!( 143 MycSignerRequestDigest::for_canonical_request(&[]) 144 .expect_err("empty request") 145 .kind(), 146 MycSignerRequestErrorKind::InvalidCanonicalRequest 147 ); 148 assert_eq!( 149 MycSignerRequestDigest::for_canonical_request(&vec![ 150 b'x'; 151 MYC_NIP46_CANONICAL_REQUEST_MAX_BYTES 152 + 1 153 ]) 154 .expect_err("oversized request") 155 .kind(), 156 MycSignerRequestErrorKind::InvalidCanonicalRequest 157 ); 158 159 assert!(MycRequestReceivedAtUnixMs::new(i64::MAX.unsigned_abs()).is_ok()); 160 for invalid in [0, i64::MAX.unsigned_abs() + 1] { 161 assert_eq!( 162 MycRequestReceivedAtUnixMs::new(invalid) 163 .expect_err("invalid time") 164 .kind(), 165 MycSignerRequestErrorKind::InvalidReceivedAt 166 ); 167 } 168 169 let methods = [ 170 (MycSignerRequestMethod::Connect, "connect"), 171 (MycSignerRequestMethod::GetPublicKey, "get_public_key"), 172 ( 173 MycSignerRequestMethod::GetSessionCapability, 174 "get_session_capability", 175 ), 176 (MycSignerRequestMethod::SignEvent, "sign_event"), 177 (MycSignerRequestMethod::Nip04Encrypt, "nip04_encrypt"), 178 (MycSignerRequestMethod::Nip04Decrypt, "nip04_decrypt"), 179 (MycSignerRequestMethod::Nip44Encrypt, "nip44_encrypt"), 180 (MycSignerRequestMethod::Nip44Decrypt, "nip44_decrypt"), 181 (MycSignerRequestMethod::Ping, "ping"), 182 (MycSignerRequestMethod::SwitchRelays, "switch_relays"), 183 (MycSignerRequestMethod::Logout, "logout"), 184 ]; 185 assert_eq!( 186 methods.map(|(method, _)| method.as_str()), 187 methods.map(|(_, wire)| wire) 188 ); 189 190 assert_eq!( 191 hex(MycSignerRequestDigest::for_canonical_request(REQUEST_BYTES) 192 .expect("digest") 193 .as_bytes()), 194 "378d4f7906aed41e5af96c7000dfc57d9c4c4c9ad3d94b84e985621c25e727f2" 195 ); 196 let nonce = MycSignerOperationNonce::from_injected_entropy([0x5a; 32]); 197 assert_eq!(format!("{nonce:?}"), "MycSignerOperationNonce([redacted])"); 198 199 let error = MycNip46RequestId::new(" secret\n").expect_err("invalid input"); 200 assert!(Error::source(&error).is_none()); 201 let request = request( 202 "request-01", 203 0x44, 204 MycSignerRequestMethod::Ping, 205 REQUEST_BYTES, 206 1, 207 1, 208 ); 209 let rendered = format!("{request:?} {error} {error:?}"); 210 for secret in [CLIENT_PUBLIC_KEY, "request-01", "secret", "378d4f79"] { 211 assert!(!rendered.contains(secret)); 212 } 213 } 214 215 #[tokio::test] 216 async fn request_admission_is_atomic_idempotent_conflict_aware_and_restart_stable() { 217 let directory = tempfile::tempdir().expect("temporary root"); 218 let runtime = runtime(directory.path()); 219 prepare_state_directory(&runtime); 220 let metadata = metadata(&runtime); 221 let (applied_at, build) = migration_evidence(); 222 initialize_myc_state(&runtime, &metadata, applied_at, &build) 223 .await 224 .expect("state initialization"); 225 let host = open_myc_state_read_write(&runtime, &metadata, applied_at, &build) 226 .await 227 .expect("writable host"); 228 229 let first = request( 230 "request-01", 231 0x44, 232 MycSignerRequestMethod::Ping, 233 REQUEST_BYTES, 234 100, 235 100, 236 ); 237 let admitted = host 238 .repository() 239 .admit_signer_request(&first) 240 .await 241 .expect("first admission"); 242 assert!(matches!(admitted, MycSignerRequestAdmission::Admitted(_))); 243 assert_eq!(admitted.record().replay_count(), 0); 244 assert_eq!(admitted.record().conflict_count(), 0); 245 assert_eq!( 246 hex(admitted.record().operation_id().as_bytes()), 247 "c72e6b8e1824b94e8ce6284cff532895c3e8b80c7859e02decfc7fda88134444" 248 ); 249 assert_eq!( 250 hex(admitted.record().correlation_id().as_bytes()), 251 "87d551e4c196fded2fa4d9335655711f64da873c552e7338f8e99541eb871654" 252 ); 253 assert_ne!( 254 admitted.record().operation_id().as_bytes(), 255 admitted.record().correlation_id().as_bytes() 256 ); 257 let first_operation_id = admitted.record().operation_id(); 258 let first_correlation_id = admitted.record().correlation_id(); 259 260 let same_event = request( 261 "request-01", 262 0x44, 263 MycSignerRequestMethod::Ping, 264 REQUEST_BYTES, 265 101, 266 101, 267 ); 268 let replay = host 269 .repository() 270 .admit_signer_request(&same_event) 271 .await 272 .expect("same-event replay"); 273 assert!(matches!(replay, MycSignerRequestAdmission::ExactReplay(_))); 274 assert_eq!(replay.record().replay_count(), 1); 275 assert_eq!(replay.record().operation_id(), first_operation_id); 276 assert_eq!(replay.record().correlation_id(), first_correlation_id); 277 278 let new_event = request( 279 "request-01", 280 0x45, 281 MycSignerRequestMethod::Ping, 282 REQUEST_BYTES, 283 102, 284 102, 285 ); 286 let replay = host 287 .repository() 288 .admit_signer_request(&new_event) 289 .await 290 .expect("new-event replay"); 291 assert!(matches!(replay, MycSignerRequestAdmission::ExactReplay(_))); 292 assert_eq!(replay.record().replay_count(), 2); 293 294 let changed_payload = request( 295 "request-01", 296 0x46, 297 MycSignerRequestMethod::GetPublicKey, 298 b"{\"id\":\"request-01\",\"method\":\"get_public_key\",\"params\":[]}", 299 103, 300 103, 301 ); 302 let conflict = host 303 .repository() 304 .admit_signer_request(&changed_payload) 305 .await 306 .expect("request conflict"); 307 assert!(matches!( 308 conflict, 309 MycSignerRequestAdmission::ConflictingReuse(_) 310 )); 311 assert_eq!(conflict.record().method(), MycSignerRequestMethod::Ping); 312 assert_eq!(conflict.record().conflict_count(), 1); 313 314 let reused_event = request( 315 "request-02", 316 0x44, 317 MycSignerRequestMethod::Ping, 318 b"{\"id\":\"request-02\",\"method\":\"ping\",\"params\":[]}", 319 104, 320 104, 321 ); 322 let conflict = host 323 .repository() 324 .admit_signer_request(&reused_event) 325 .await 326 .expect("event conflict"); 327 assert!(matches!( 328 conflict, 329 MycSignerRequestAdmission::ConflictingReuse(_) 330 )); 331 assert_eq!(conflict.record().operation_id(), first_operation_id); 332 let rendered = format!("{admitted:?} {:?}", admitted.record()); 333 for secret in [CLIENT_PUBLIC_KEY, "request-01", "c72e6b8e"] { 334 assert!(!rendered.contains(secret)); 335 } 336 host.close().await.expect("close after first lifecycle"); 337 338 let host = open_myc_state_read_write(&runtime, &metadata, applied_at, &build) 339 .await 340 .expect("reopened writable host"); 341 let after_reopen = request( 342 "request-01", 343 0x47, 344 MycSignerRequestMethod::Ping, 345 REQUEST_BYTES, 346 105, 347 105, 348 ); 349 let replay = host 350 .repository() 351 .admit_signer_request(&after_reopen) 352 .await 353 .expect("replay after reopen"); 354 assert!(matches!(replay, MycSignerRequestAdmission::ExactReplay(_))); 355 assert_eq!(replay.record().replay_count(), 3); 356 assert_eq!(replay.record().conflict_count(), 1); 357 host.close().await.expect("final close"); 358 359 let options = SqliteConnectOptions::new() 360 .filename(runtime.artifacts().state_database()) 361 .create_if_missing(false) 362 .read_only(true) 363 .disable_statement_logging(); 364 let mut connection = sqlx::SqliteConnection::connect_with(&options) 365 .await 366 .expect("test inspection connection"); 367 assert_eq!( 368 sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM nip46_requests") 369 .fetch_one(&mut connection) 370 .await 371 .expect("request count"), 372 1 373 ); 374 assert_eq!( 375 sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM nip46_request_dedup") 376 .fetch_one(&mut connection) 377 .await 378 .expect("dedup count"), 379 4 380 ); 381 let event_conflicts = sqlx::query_scalar::<_, i64>( 382 "SELECT conflict_count FROM nip46_request_dedup \ 383 WHERE dedup_kind = 'event' AND identity_sha256 = ?", 384 ) 385 .bind([0x44_u8; 32].as_slice()) 386 .fetch_one(&mut connection) 387 .await 388 .expect("event conflict count"); 389 assert_eq!(event_conflicts, 1); 390 assert!( 391 sqlx::query("UPDATE nip46_requests SET received_at_unix_ms = 999") 392 .execute(&mut connection) 393 .await 394 .is_err() 395 ); 396 connection.close().await.expect("inspection close"); 397 } 398 399 #[tokio::test] 400 async fn concurrent_identical_admission_creates_one_request_and_bounded_replay_evidence() { 401 let directory = tempfile::tempdir().expect("temporary root"); 402 let runtime = runtime(directory.path()); 403 prepare_state_directory(&runtime); 404 let metadata = metadata(&runtime); 405 let (applied_at, build) = migration_evidence(); 406 initialize_myc_state(&runtime, &metadata, applied_at, &build) 407 .await 408 .expect("state initialization"); 409 let host = open_myc_state_read_write(&runtime, &metadata, applied_at, &build) 410 .await 411 .expect("writable host"); 412 let request = request( 413 "request-concurrent", 414 0x55, 415 MycSignerRequestMethod::Ping, 416 b"concurrent", 417 200, 418 200, 419 ); 420 let repository = host.repository(); 421 422 let (a, b, c, d, e, f, g, h) = tokio::join!( 423 repository.admit_signer_request(&request), 424 repository.admit_signer_request(&request), 425 repository.admit_signer_request(&request), 426 repository.admit_signer_request(&request), 427 repository.admit_signer_request(&request), 428 repository.admit_signer_request(&request), 429 repository.admit_signer_request(&request), 430 repository.admit_signer_request(&request), 431 ); 432 let outcomes = [a, b, c, d, e, f, g, h] 433 .into_iter() 434 .collect::<Result<Vec<_>, _>>() 435 .expect("concurrent admissions"); 436 assert_eq!( 437 outcomes 438 .iter() 439 .filter(|outcome| matches!(outcome, MycSignerRequestAdmission::Admitted(_))) 440 .count(), 441 1 442 ); 443 assert_eq!( 444 outcomes 445 .iter() 446 .filter(|outcome| matches!(outcome, MycSignerRequestAdmission::ExactReplay(_))) 447 .count(), 448 7 449 ); 450 let final_record = outcomes 451 .iter() 452 .max_by_key(|outcome| outcome.record().replay_count()) 453 .expect("final replay record") 454 .record(); 455 assert_eq!(final_record.replay_count(), 7); 456 assert_eq!(final_record.conflict_count(), 0); 457 host.close().await.expect("host close"); 458 } 459 460 #[tokio::test] 461 async fn exact_schema_v3_state_advances_to_v12_before_request_admission() { 462 let directory = tempfile::tempdir().expect("temporary root"); 463 let runtime = runtime(directory.path()); 464 prepare_state_directory(&runtime); 465 let metadata = metadata(&runtime); 466 let (applied_at, build) = migration_evidence(); 467 initialize_myc_state(&runtime, &metadata, applied_at, &build) 468 .await 469 .expect("state initialization"); 470 471 let options = SqliteConnectOptions::new() 472 .filename(runtime.artifacts().state_database()) 473 .create_if_missing(false) 474 .disable_statement_logging(); 475 let mut connection = sqlx::SqliteConnection::connect_with(&options) 476 .await 477 .expect("downgrade fixture connection"); 478 let shared_update = sqlx::query_scalar::<_, String>( 479 "SELECT sql FROM sqlite_schema WHERE type = 'trigger' \ 480 AND name = 'radroots_service_metadata_guard_update'", 481 ) 482 .fetch_one(&mut connection) 483 .await 484 .expect("shared update trigger"); 485 let myc_update = sqlx::query_scalar::<_, String>( 486 "SELECT sql FROM sqlite_schema WHERE type = 'trigger' \ 487 AND name = 'myc_state_metadata_no_update'", 488 ) 489 .fetch_one(&mut connection) 490 .await 491 .expect("Myc update trigger"); 492 let migration_delete = sqlx::query_scalar::<_, String>( 493 "SELECT sql FROM sqlite_schema WHERE type = 'trigger' \ 494 AND name = 'schema_migrations_no_delete'", 495 ) 496 .fetch_one(&mut connection) 497 .await 498 .expect("migration delete trigger"); 499 for sql in [ 500 "DROP TRIGGER radroots_service_metadata_guard_update", 501 "DROP TRIGGER myc_state_metadata_no_update", 502 "DROP TRIGGER schema_migrations_no_delete", 503 "DROP TRIGGER connection_rate_windows_guard_update", 504 "DROP TRIGGER nip46_request_audit_no_update", 505 "DROP TRIGGER operation_audit_no_update", 506 "DROP TRIGGER myc_audit_state_no_delete", 507 "DROP TRIGGER myc_audit_state_guard_update", 508 "DROP TRIGGER connection_auth_challenges_no_delete", 509 "DROP TRIGGER connection_auth_challenges_guard_update", 510 "DROP TRIGGER nip46_request_decisions_no_delete", 511 "DROP TRIGGER nip46_request_decisions_guard_update", 512 "DROP TRIGGER connection_permissions_no_delete", 513 "DROP TRIGGER connection_permissions_no_update", 514 "DROP TRIGGER connections_no_delete", 515 "DROP TRIGGER connections_guard_update", 516 "DROP TRIGGER nip46_pending_responses_no_delete", 517 "DROP TRIGGER nip46_pending_responses_no_update", 518 "DROP TRIGGER nip46_signed_responses_guard_pending_insert", 519 "DROP TRIGGER nip46_pending_responses_guard_insert", 520 "DROP TABLE nip46_pending_responses", 521 "DROP TRIGGER myc_admin_operations_guard_update", 522 "DROP TABLE myc_admin_operations", 523 "DROP TRIGGER myc_config_bindings_no_delete", 524 "DROP TRIGGER myc_config_bindings_no_update", 525 "DROP TRIGGER myc_config_bindings_guard_insert", 526 "DROP TABLE myc_config_bindings", 527 "DROP TRIGGER nip46_signed_responses_no_delete", 528 "DROP TRIGGER nip46_signed_responses_no_update", 529 "DROP TABLE nip46_signed_responses", 530 "DROP TRIGGER nip46_operation_commits_no_delete", 531 "DROP TRIGGER nip46_operation_commits_no_update", 532 "DROP TABLE nip46_operation_commits", 533 "DROP TABLE discovery_publication_state", 534 "DROP TABLE discovery_documents", 535 "DROP TABLE discovery_desired_state", 536 "DROP TABLE delivery_attempts", 537 "DROP TABLE delivery_targets", 538 "DROP TABLE delivery_jobs", 539 "DROP TABLE nip46_request_audit", 540 "DROP TABLE operation_audit", 541 "DROP TABLE connection_rate_windows", 542 "DROP TABLE myc_audit_state", 543 "DROP TABLE nip46_request_decisions", 544 "DROP TABLE connection_auth_challenges", 545 "DROP TABLE connection_permissions", 546 "DROP TABLE connections", 547 "UPDATE radroots_service_metadata SET state_schema_version = 3 WHERE singleton = 1", 548 "UPDATE myc_state_metadata SET state_contract_version = 3 WHERE singleton = 1", 549 "DELETE FROM schema_migrations WHERE version IN (4, 5, 6, 7, 8, 9, 10, 11, 12)", 550 ] { 551 sqlx::query(sql) 552 .execute(&mut connection) 553 .await 554 .expect("construct exact schema-v3 fixture"); 555 } 556 for sql in [&shared_update, &myc_update, &migration_delete] { 557 sqlx::raw_sql(sqlx::AssertSqlSafe(sql.as_str())) 558 .execute(&mut connection) 559 .await 560 .expect("restore exact governed trigger"); 561 } 562 connection.close().await.expect("fixture connection close"); 563 564 let host = open_myc_state_read_write(&runtime, &metadata, applied_at, &build) 565 .await 566 .expect("schema-v3 upgrade"); 567 let admitted = host 568 .repository() 569 .admit_signer_request(&request( 570 "request-after-v3", 571 0x66, 572 MycSignerRequestMethod::Ping, 573 b"after-v3", 574 0x66, 575 300, 576 )) 577 .await 578 .expect("request admission after migration"); 579 assert!(matches!(admitted, MycSignerRequestAdmission::Admitted(_))); 580 host.close().await.expect("upgraded host close"); 581 } 582 583 #[tokio::test] 584 async fn persisted_operation_entropy_tampering_fails_closed_as_a_binding_error() { 585 let directory = tempfile::tempdir().expect("temporary root"); 586 let runtime = runtime(directory.path()); 587 prepare_state_directory(&runtime); 588 let metadata = metadata(&runtime); 589 let (applied_at, build) = migration_evidence(); 590 initialize_myc_state(&runtime, &metadata, applied_at, &build) 591 .await 592 .expect("state initialization"); 593 let host = open_myc_state_read_write(&runtime, &metadata, applied_at, &build) 594 .await 595 .expect("writable host"); 596 let request = request( 597 "request-tamper", 598 0x77, 599 MycSignerRequestMethod::Ping, 600 b"tamper-check", 601 0x77, 602 400, 603 ); 604 host.repository() 605 .admit_signer_request(&request) 606 .await 607 .expect("initial request admission"); 608 host.close().await.expect("host close before tamper"); 609 610 let options = SqliteConnectOptions::new() 611 .filename(runtime.artifacts().state_database()) 612 .create_if_missing(false) 613 .disable_statement_logging(); 614 let mut connection = sqlx::SqliteConnection::connect_with(&options) 615 .await 616 .expect("tamper fixture connection"); 617 let request_guard = sqlx::query_scalar::<_, String>( 618 "SELECT sql FROM sqlite_schema WHERE type = 'trigger' \ 619 AND name = 'nip46_requests_no_update'", 620 ) 621 .fetch_one(&mut connection) 622 .await 623 .expect("request guard"); 624 sqlx::query("DROP TRIGGER nip46_requests_no_update") 625 .execute(&mut connection) 626 .await 627 .expect("drop request guard for corruption fixture"); 628 sqlx::query("UPDATE nip46_requests SET operation_nonce = zeroblob(32)") 629 .execute(&mut connection) 630 .await 631 .expect("corrupt persisted operation nonce"); 632 sqlx::raw_sql(sqlx::AssertSqlSafe(request_guard.as_str())) 633 .execute(&mut connection) 634 .await 635 .expect("restore request guard"); 636 connection.close().await.expect("fixture connection close"); 637 638 let host = open_myc_state_read_write(&runtime, &metadata, applied_at, &build) 639 .await 640 .expect("reopen structurally valid state"); 641 let error = host 642 .repository() 643 .admit_signer_request(&request) 644 .await 645 .expect_err("tampered operation binding"); 646 assert_eq!(error.kind(), myc::MycStateRepositoryErrorKind::Binding); 647 host.close().await.expect("host close after rejection"); 648 } 649 650 #[test] 651 fn request_store_is_sealed_transactional_bounded_and_independent_of_external_effects() { 652 assert!(REQUEST_SOURCE.contains("ServiceSqliteTransaction<'_>")); 653 assert!(REQUEST_SOURCE.contains("require_expected_metadata(transaction, &expected)")); 654 assert!(REQUEST_SOURCE.contains("CASE WHEN typeof(r.operation_id) = 'blob'")); 655 assert!(REQUEST_SOURCE.contains("END AS logical_operation_id")); 656 assert!(REQUEST_SOURCE.contains("LIMIT 2")); 657 assert!(REQUEST_SOURCE.contains("replay_count < 9223372036854775807")); 658 assert!(REQUEST_SOURCE.contains("conflict_count < 9223372036854775807")); 659 assert!(CATALOG_SOURCE.contains("CREATE TABLE nip46_requests")); 660 assert!(CATALOG_SOURCE.contains("CREATE TABLE nip46_request_dedup")); 661 assert!(CATALOG_SOURCE.contains("nip46_request_dedup_guard_update")); 662 for forbidden in [ 663 "SqlitePool", 664 "SqliteConnection", 665 "BEGIN ", 666 "COMMIT", 667 "ROLLBACK", 668 "std::fs", 669 "std::env", 670 "SystemTime", 671 "Instant::now", 672 "tokio::spawn", 673 "spawn_blocking", 674 "rand::", 675 "getrandom", 676 "SystemEntropy", 677 "reqwest", 678 "RelayPool", 679 "provider.await", 680 ] { 681 assert!( 682 !REQUEST_SOURCE.contains(forbidden), 683 "found forbidden request-store authority `{forbidden}`" 684 ); 685 } 686 }