services_hardening_connection_state.rs (63272B)
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_AUDIT_PAGE_MAX_ITEMS, MYC_AUDIT_RETENTION_MAX_MS, 8 MYC_AUTHORIZATION_CHALLENGE_URL_MAX_BYTES, MYC_COMPACTION_MAX_ROWS, 9 MYC_CONNECTION_PERMISSION_MAX_COUNT, MYC_RATE_MAX_ATTEMPTS, MYC_RATE_MAX_TRACKED_SUBJECTS, 10 MYC_RATE_RELAY_ID_MAX_BYTES, MYC_RATE_RETENTION_MAX_MS, MYC_RATE_WINDOW_MAX_MS, 11 MYC_STATE_SCHEMA_VERSION, MycAuditCorrelationId, MycAuditKind, MycAuditOutcome, 12 MycAuditPageLimit, MycAuditReasonCode, MycAuthorizationChallengeAdmission, 13 MycAuthorizationChallengeNonce, MycAuthorizationChallengeRequest, 14 MycAuthorizationChallengeState, MycAuthorizationChallengeUrl, MycConfigProfile, 15 MycConnectionAdmission, MycConnectionAdmissionPolicy, MycConnectionAdmissionRequest, 16 MycConnectionNonce, MycConnectionOperatorDecision, MycConnectionPermission, 17 MycConnectionPermissionSet, MycConnectionPolicyGeneration, MycConnectionStateErrorKind, 18 MycConnectionStatus, MycConnectionTimeUnixMs, MycGovernanceCompactionPolicy, 19 MycGovernanceStateErrorKind, MycNip46ClientPublicKey, MycNip46EventId, MycNip46RequestId, 20 MycRateLimitClass, MycRateLimitPolicy, MycRateRelayId, MycRequestReceivedAtUnixMs, 21 MycSignerOperationId, MycSignerOperationNonce, MycSignerRequest, MycSignerRequestDigest, 22 MycSignerRequestMethod, MycStateMetadata, MycStateRepository, MycStateRepositoryErrorKind, 23 RadrootsHostEnvironment, RadrootsPathResolver, RadrootsPlatform, initialize_myc_state, 24 open_myc_state_read_write, parse_myc_cli_v1_from, parse_myc_config_v1, 25 resolve_myc_runtime_context, 26 }; 27 use radroots_service_sqlite::{MigrationAppliedAtUnixSeconds, MigrationBuildIdentity}; 28 use radroots_storage::event::SourceGeneration; 29 use sqlx::{ConnectOptions, Connection, sqlite::SqliteConnectOptions}; 30 31 const CONFIG_EXAMPLE: &[u8] = 32 include_bytes!("../contracts/services_hardening/config.v1.example.toml"); 33 const CONNECTION_SOURCE: &str = include_str!("../src/state_connection.rs"); 34 const GOVERNANCE_SOURCE: &str = include_str!("../src/state_governance.rs"); 35 const CLIENT_PUBLIC_KEY: &str = "2222222222222222222222222222222222222222222222222222222222222222"; 36 const TRUSTED_CLIENT_PUBLIC_KEY: &str = 37 "7777777777777777777777777777777777777777777777777777777777777777"; 38 const DENIED_CLIENT_PUBLIC_KEY: &str = 39 "8888888888888888888888888888888888888888888888888888888888888888"; 40 41 fn runtime(root: &Path) -> myc::MycRuntimeContext { 42 let root = root.to_str().expect("UTF-8 temporary root"); 43 let invocation = parse_myc_cli_v1_from([ 44 "myc", 45 "--profile", 46 "repo-local", 47 "--instance", 48 "primary", 49 "--repo-local-root", 50 root, 51 "run", 52 ]) 53 .expect("valid test invocation"); 54 resolve_myc_runtime_context( 55 &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()), 56 &invocation, 57 ) 58 .expect("runtime context") 59 } 60 61 fn prepare_state_directory(runtime: &myc::MycRuntimeContext) { 62 let directory = runtime.context().paths().state(); 63 fs::create_dir_all(directory).expect("state directory"); 64 fs::set_permissions(directory, fs::Permissions::from_mode(0o700)).expect("state mode"); 65 } 66 67 fn metadata(runtime: &myc::MycRuntimeContext) -> MycStateMetadata { 68 metadata_from_bytes(runtime, CONFIG_EXAMPLE) 69 } 70 71 fn metadata_from_bytes(runtime: &myc::MycRuntimeContext, bytes: &[u8]) -> MycStateMetadata { 72 let configuration = 73 parse_myc_config_v1(bytes, MycConfigProfile::RepoLocal).expect("configuration"); 74 MycStateMetadata::new( 75 runtime, 76 &configuration, 77 SourceGeneration::new([0x5a; 32]).expect("generation"), 78 1_725_000_000_000, 79 ) 80 .expect("metadata") 81 } 82 83 fn migration_evidence() -> (MigrationAppliedAtUnixSeconds, MigrationBuildIdentity) { 84 let applied_at = MigrationAppliedAtUnixSeconds::new(1_725_000_000).expect("migration time"); 85 let build = MigrationBuildIdentity::new( 86 env!("CARGO_PKG_VERSION"), 87 "1111111111111111111111111111111111111111", 88 "053d0c750bf9cd683c6ea37cefe7e79617ba629f", 89 "rustc-test", 90 "test-target", 91 "service-host", 92 1, 93 MYC_STATE_SCHEMA_VERSION, 94 1, 95 1, 96 1, 97 ) 98 .expect("build identity"); 99 (applied_at, build) 100 } 101 102 fn client() -> MycNip46ClientPublicKey { 103 MycNip46ClientPublicKey::new(CLIENT_PUBLIC_KEY).expect("client identity") 104 } 105 106 fn trusted_client() -> MycNip46ClientPublicKey { 107 MycNip46ClientPublicKey::new(TRUSTED_CLIENT_PUBLIC_KEY).expect("trusted client identity") 108 } 109 110 fn denied_client() -> MycNip46ClientPublicKey { 111 MycNip46ClientPublicKey::new(DENIED_CLIENT_PUBLIC_KEY).expect("denied client identity") 112 } 113 114 fn client_for_policy(policy: MycConnectionAdmissionPolicy) -> MycNip46ClientPublicKey { 115 match policy { 116 MycConnectionAdmissionPolicy::Trusted => trusted_client(), 117 MycConnectionAdmissionPolicy::ExplicitApproval => client(), 118 MycConnectionAdmissionPolicy::Denied => denied_client(), 119 } 120 } 121 122 fn permission_set(permissions: &[MycConnectionPermission]) -> MycConnectionPermissionSet { 123 MycConnectionPermissionSet::new(permissions).expect("permission set") 124 } 125 126 fn time(value: u64) -> MycConnectionTimeUnixMs { 127 MycConnectionTimeUnixMs::new(value).expect("connection time") 128 } 129 130 fn policy_generation(value: u64) -> MycConnectionPolicyGeneration { 131 MycConnectionPolicyGeneration::new(value).expect("policy generation") 132 } 133 134 fn audit_correlation(byte: u8) -> MycAuditCorrelationId { 135 MycAuditCorrelationId::new([byte; 32]) 136 } 137 138 async fn admit_request( 139 repository: &MycStateRepository<'_>, 140 client_public_key: MycNip46ClientPublicKey, 141 request_id: &str, 142 event_byte: u8, 143 method: MycSignerRequestMethod, 144 nonce_byte: u8, 145 received_at: u64, 146 ) -> MycSignerOperationId { 147 let canonical = format!( 148 "{{\"id\":\"{request_id}\",\"method\":\"{}\"}}", 149 method.as_str() 150 ); 151 let request = MycSignerRequest::new( 152 client_public_key, 153 MycNip46RequestId::new(request_id).expect("request ID"), 154 MycNip46EventId::from_bytes([event_byte; 32]), 155 method, 156 MycSignerRequestDigest::for_canonical_request(canonical.as_bytes()) 157 .expect("request digest"), 158 MycSignerOperationNonce::from_injected_entropy([nonce_byte; 32]), 159 MycRequestReceivedAtUnixMs::new(received_at).expect("received time"), 160 ); 161 repository 162 .admit_signer_request(&request) 163 .await 164 .expect("request admission") 165 .record() 166 .operation_id() 167 } 168 169 fn connection_request( 170 operation_id: MycSignerOperationId, 171 permissions: MycConnectionPermissionSet, 172 generation: u64, 173 nonce_byte: u8, 174 observed_at: u64, 175 authorized_until: Option<u64>, 176 policy: MycConnectionAdmissionPolicy, 177 ) -> MycConnectionAdmissionRequest { 178 MycConnectionAdmissionRequest::new( 179 operation_id, 180 client_for_policy(policy), 181 permissions, 182 policy_generation(generation), 183 MycConnectionNonce::from_injected_entropy([nonce_byte; 32]), 184 time(observed_at), 185 authorized_until.map(time), 186 policy, 187 MycRateRelayId::new("primary").expect("relay ID"), 188 ) 189 .expect("connection request") 190 } 191 192 fn hex(bytes: &[u8]) -> String { 193 bytes.iter().map(|byte| format!("{byte:02x}")).collect() 194 } 195 196 #[test] 197 fn connection_inputs_are_closed_bounded_canonical_and_redacted() { 198 let maximum = (0..MYC_CONNECTION_PERMISSION_MAX_COUNT) 199 .map(|kind| MycConnectionPermission::SignEvent(u32::try_from(kind).expect("kind"))) 200 .collect::<Vec<_>>(); 201 let permissions = MycConnectionPermissionSet::new(&maximum).expect("maximum permissions"); 202 assert_eq!( 203 permissions.permissions().len(), 204 MYC_CONNECTION_PERMISSION_MAX_COUNT 205 ); 206 assert_eq!( 207 MycConnectionPermissionSet::new(&[ 208 MycConnectionPermission::Ping, 209 MycConnectionPermission::Ping, 210 ]) 211 .expect_err("duplicate permission") 212 .kind(), 213 MycConnectionStateErrorKind::InvalidPermissionSet 214 ); 215 let excessive = (0..=MYC_CONNECTION_PERMISSION_MAX_COUNT) 216 .map(|kind| MycConnectionPermission::SignEvent(u32::try_from(kind).expect("kind"))) 217 .collect::<Vec<_>>(); 218 assert_eq!( 219 MycConnectionPermissionSet::new(&excessive) 220 .expect_err("excessive permissions") 221 .kind(), 222 MycConnectionStateErrorKind::InvalidPermissionSet 223 ); 224 assert!(MycConnectionPermissionSet::new(&[]).is_ok()); 225 assert!( 226 MycConnectionPermissionSet::new(&[MycConnectionPermission::SignEvent(u32::MAX)]).is_ok() 227 ); 228 229 assert!(MycConnectionPolicyGeneration::new(i64::MAX.unsigned_abs()).is_ok()); 230 assert!(MycConnectionTimeUnixMs::new(i64::MAX.unsigned_abs()).is_ok()); 231 for invalid in [0, i64::MAX.unsigned_abs() + 1] { 232 assert_eq!( 233 MycConnectionPolicyGeneration::new(invalid) 234 .expect_err("invalid policy generation") 235 .kind(), 236 MycConnectionStateErrorKind::InvalidPolicyGeneration 237 ); 238 assert_eq!( 239 MycConnectionTimeUnixMs::new(invalid) 240 .expect_err("invalid time") 241 .kind(), 242 MycConnectionStateErrorKind::InvalidTime 243 ); 244 } 245 246 for accepted in [ 247 "https://operator.example/authorize", 248 "http://localhost:8080/authorize", 249 "http://127.0.0.1/authorize", 250 "http://[::1]/authorize", 251 ] { 252 assert_eq!( 253 MycAuthorizationChallengeUrl::new(accepted) 254 .expect("accepted URL") 255 .as_str(), 256 accepted 257 ); 258 } 259 let maximum_url = format!( 260 "https://operator.example/{}", 261 "a".repeat(MYC_AUTHORIZATION_CHALLENGE_URL_MAX_BYTES - 25) 262 ); 263 assert_eq!(maximum_url.len(), MYC_AUTHORIZATION_CHALLENGE_URL_MAX_BYTES); 264 assert!(MycAuthorizationChallengeUrl::new(&maximum_url).is_ok()); 265 for invalid in [ 266 "", 267 "https://operator.example", 268 "http://operator.example/authorize", 269 "https://user@operator.example/authorize", 270 "https://operator.example/authorize#fragment", 271 &format!("https://operator.example/{}", "a".repeat(2_100)), 272 ] { 273 assert_eq!( 274 MycAuthorizationChallengeUrl::new(invalid) 275 .expect_err("invalid URL") 276 .kind(), 277 MycConnectionStateErrorKind::InvalidChallengeUrl 278 ); 279 } 280 281 let error = MycConnectionPolicyGeneration::new(0).expect_err("invalid generation"); 282 assert!(Error::source(&error).is_none()); 283 let rendered = format!( 284 "{permissions:?} {:?} {:?} {error} {error:?}", 285 MycConnectionNonce::from_injected_entropy([0x5a; 32]), 286 MycAuthorizationChallengeUrl::new("https://operator.example/secret").expect("URL") 287 ); 288 for secret in ["operator.example", "secret", "5a5a5a", CLIENT_PUBLIC_KEY] { 289 assert!(!rendered.contains(secret)); 290 } 291 } 292 293 #[test] 294 fn governance_inputs_are_closed_bounded_and_redacted() { 295 assert!( 296 MycRateLimitPolicy::new( 297 MycRateLimitClass::ConnectionAdmission, 298 MYC_RATE_WINDOW_MAX_MS, 299 MYC_RATE_MAX_ATTEMPTS, 300 MYC_RATE_RETENTION_MAX_MS, 301 MYC_RATE_MAX_TRACKED_SUBJECTS, 302 ) 303 .is_ok() 304 ); 305 for invalid in [ 306 MycRateLimitPolicy::new(MycRateLimitClass::ConnectionAdmission, 0, 1, 1, 1), 307 MycRateLimitPolicy::new( 308 MycRateLimitClass::ConnectionAdmission, 309 MYC_RATE_WINDOW_MAX_MS + 1, 310 1, 311 MYC_RATE_WINDOW_MAX_MS + 1, 312 1, 313 ), 314 MycRateLimitPolicy::new(MycRateLimitClass::ConnectionAdmission, 1, 0, 1, 1), 315 MycRateLimitPolicy::new( 316 MycRateLimitClass::ConnectionAdmission, 317 1, 318 MYC_RATE_MAX_ATTEMPTS + 1, 319 1, 320 1, 321 ), 322 MycRateLimitPolicy::new(MycRateLimitClass::ConnectionAdmission, 2, 1, 1, 1), 323 MycRateLimitPolicy::new( 324 MycRateLimitClass::ConnectionAdmission, 325 1, 326 1, 327 MYC_RATE_RETENTION_MAX_MS + 1, 328 1, 329 ), 330 MycRateLimitPolicy::new(MycRateLimitClass::ConnectionAdmission, 1, 1, 1, 0), 331 MycRateLimitPolicy::new( 332 MycRateLimitClass::ConnectionAdmission, 333 1, 334 1, 335 1, 336 MYC_RATE_MAX_TRACKED_SUBJECTS + 1, 337 ), 338 ] { 339 assert_eq!( 340 invalid.expect_err("invalid rate policy").kind(), 341 MycGovernanceStateErrorKind::InvalidRatePolicy 342 ); 343 } 344 345 let maximum_relay = format!("a{}", "1".repeat(MYC_RATE_RELAY_ID_MAX_BYTES - 1)); 346 assert!(MycRateRelayId::new(&maximum_relay).is_ok()); 347 for invalid in [ 348 "", 349 "A", 350 "relay-name", 351 "relay__name", 352 "relay_", 353 &format!("a{maximum_relay}"), 354 ] { 355 assert_eq!( 356 MycRateRelayId::new(invalid) 357 .expect_err("invalid relay ID") 358 .kind(), 359 MycGovernanceStateErrorKind::InvalidRelayId 360 ); 361 } 362 assert!(MycAuditPageLimit::new(MYC_AUDIT_PAGE_MAX_ITEMS).is_ok()); 363 for value in [0, MYC_AUDIT_PAGE_MAX_ITEMS + 1] { 364 assert_eq!( 365 MycAuditPageLimit::new(value) 366 .expect_err("invalid page limit") 367 .kind(), 368 MycGovernanceStateErrorKind::InvalidPageLimit 369 ); 370 } 371 assert!( 372 MycGovernanceCompactionPolicy::new(MYC_AUDIT_RETENTION_MAX_MS, MYC_COMPACTION_MAX_ROWS,) 373 .is_ok() 374 ); 375 for invalid in [ 376 MycGovernanceCompactionPolicy::new(0, 1), 377 MycGovernanceCompactionPolicy::new(MYC_AUDIT_RETENTION_MAX_MS + 1, 1), 378 MycGovernanceCompactionPolicy::new(1, 0), 379 MycGovernanceCompactionPolicy::new(1, MYC_COMPACTION_MAX_ROWS + 1), 380 ] { 381 let error = invalid.expect_err("invalid compaction policy"); 382 assert_eq!( 383 error.kind(), 384 MycGovernanceStateErrorKind::InvalidCompactionPolicy 385 ); 386 assert!(Error::source(&error).is_none()); 387 } 388 389 let relay = MycRateRelayId::new("relay_secret_123").expect("relay ID"); 390 let policy = 391 MycRateLimitPolicy::new(MycRateLimitClass::ChallengeCreation, 7, 3, 9, 11).expect("policy"); 392 let rendered = format!("{relay:?} {policy:?} {:?}", audit_correlation(0x91)); 393 for secret in ["relay_secret_123", "[145, 145", "window_ms", "retention_ms"] { 394 assert!(!rendered.contains(secret)); 395 } 396 } 397 398 #[tokio::test] 399 async fn rate_windows_audit_pagination_retention_and_compaction_are_durable_and_bounded() { 400 let directory = tempfile::tempdir().expect("temporary root"); 401 let runtime = runtime(directory.path()); 402 prepare_state_directory(&runtime); 403 let configuration = std::str::from_utf8(CONFIG_EXAMPLE) 404 .expect("UTF-8 configuration") 405 .replacen( 406 "[policy.retention]\nterminal_connections_ms = 604800000\nterminal_challenges_ms = 86400000\nrequest_dedup_ms = 604800000\naudit_ms = 2592000000\ncompleted_outbox_ms = 604800000", 407 "[policy.retention]\nterminal_connections_ms = 604800000\nterminal_challenges_ms = 86400000\nrequest_dedup_ms = 604800000\naudit_ms = 500\ncompleted_outbox_ms = 604800000", 408 1, 409 ) 410 .replacen( 411 "[rate_limits.connection_admission]\nscope = \"global_and_relay\"\nwindow_ms = 60000\nmax_attempts = 10\nretention_ms = 3600000\nmaximum_tracked_subjects = 4096", 412 "[rate_limits.connection_admission]\nscope = \"global_and_relay\"\nwindow_ms = 100\nmax_attempts = 1\nretention_ms = 200\nmaximum_tracked_subjects = 8", 413 1, 414 ); 415 let metadata = metadata_from_bytes(&runtime, configuration.as_bytes()); 416 let (applied_at, build) = migration_evidence(); 417 initialize_myc_state(&runtime, &metadata, applied_at, &build) 418 .await 419 .expect("state initialization"); 420 let host = open_myc_state_read_write(&runtime, &metadata, applied_at, &build) 421 .await 422 .expect("writable host"); 423 let repository = host.repository(); 424 425 let first_operation = admit_request( 426 &repository, 427 client(), 428 "rate-first", 429 0x80, 430 MycSignerRequestMethod::Connect, 431 0x81, 432 100, 433 ) 434 .await; 435 let second_operation = admit_request( 436 &repository, 437 client(), 438 "rate-second", 439 0x82, 440 MycSignerRequestMethod::Connect, 441 0x83, 442 100, 443 ) 444 .await; 445 let first_request = connection_request( 446 first_operation, 447 permission_set(&[MycConnectionPermission::Nip44Encrypt]), 448 11, 449 0x84, 450 100, 451 None, 452 MycConnectionAdmissionPolicy::ExplicitApproval, 453 ); 454 let unconfigured_relay_request = MycConnectionAdmissionRequest::new( 455 first_operation, 456 client(), 457 permission_set(&[MycConnectionPermission::Nip44Encrypt]), 458 policy_generation(11), 459 MycConnectionNonce::from_injected_entropy([0x84; 32]), 460 time(100), 461 None, 462 MycConnectionAdmissionPolicy::ExplicitApproval, 463 MycRateRelayId::new("unconfigured").expect("relay ID"), 464 ) 465 .expect("unconfigured relay request"); 466 assert_eq!( 467 repository 468 .admit_connection(&unconfigured_relay_request) 469 .await 470 .expect_err("unconfigured relay") 471 .kind(), 472 MycStateRepositoryErrorKind::Binding 473 ); 474 let second_request = connection_request( 475 second_operation, 476 permission_set(&[MycConnectionPermission::Nip44Encrypt]), 477 11, 478 0x85, 479 100, 480 None, 481 MycConnectionAdmissionPolicy::ExplicitApproval, 482 ); 483 let (first, second) = tokio::join!( 484 repository.admit_connection(&first_request), 485 repository.admit_connection(&second_request) 486 ); 487 let first = first.expect("first concurrent admission"); 488 let second = second.expect("second concurrent admission"); 489 assert_eq!( 490 usize::from(matches!(first, MycConnectionAdmission::Admitted(_))) 491 + usize::from(matches!(second, MycConnectionAdmission::Admitted(_))), 492 1 493 ); 494 assert_eq!( 495 usize::from(matches!(first, MycConnectionAdmission::RateLimited)) 496 + usize::from(matches!(second, MycConnectionAdmission::RateLimited)), 497 1 498 ); 499 let (accepted_request, rejected_request) = 500 if matches!(first, MycConnectionAdmission::Admitted(_)) { 501 (&first_request, &second_request) 502 } else { 503 (&second_request, &first_request) 504 }; 505 assert!(matches!( 506 repository 507 .admit_connection(rejected_request) 508 .await 509 .expect("stable rate-limited replay"), 510 MycConnectionAdmission::RateLimited 511 )); 512 513 let boundary_operation = admit_request( 514 &repository, 515 client(), 516 "rate-boundary", 517 0x86, 518 MycSignerRequestMethod::Connect, 519 0x87, 520 200, 521 ) 522 .await; 523 let boundary_request = connection_request( 524 boundary_operation, 525 permission_set(&[MycConnectionPermission::Nip44Encrypt]), 526 11, 527 0x88, 528 200, 529 None, 530 MycConnectionAdmissionPolicy::ExplicitApproval, 531 ); 532 assert!(matches!( 533 repository 534 .admit_connection(&boundary_request) 535 .await 536 .expect("exact-window boundary"), 537 MycConnectionAdmission::RateLimited 538 )); 539 540 let reset_operation = admit_request( 541 &repository, 542 client(), 543 "rate-reset", 544 0x89, 545 MycSignerRequestMethod::Connect, 546 0x8a, 547 201, 548 ) 549 .await; 550 let reset_request = connection_request( 551 reset_operation, 552 permission_set(&[MycConnectionPermission::Nip44Encrypt]), 553 11, 554 0x8b, 555 201, 556 None, 557 MycConnectionAdmissionPolicy::ExplicitApproval, 558 ); 559 assert!(matches!( 560 repository 561 .admit_connection(&reset_request) 562 .await 563 .expect("new window"), 564 MycConnectionAdmission::Admitted(_) 565 )); 566 567 let page_one = repository 568 .read_audit_page(MycAuditPageLimit::new(2).expect("page limit"), None, None) 569 .await 570 .expect("first audit page"); 571 assert_eq!(page_one.snapshot_sequence(), 4); 572 assert_eq!(page_one.items().len(), 2); 573 assert!( 574 page_one 575 .items() 576 .iter() 577 .all(|record| record.operation_id().is_some()) 578 ); 579 assert_eq!(page_one.items()[0].outcome(), MycAuditOutcome::Succeeded); 580 assert_eq!( 581 page_one.items()[1].reason(), 582 MycAuditReasonCode::RateLimited 583 ); 584 let page_two = repository 585 .read_audit_page( 586 MycAuditPageLimit::new(2).expect("page limit"), 587 Some(page_one.snapshot_sequence()), 588 page_one.next_before_sequence(), 589 ) 590 .await 591 .expect("second audit page"); 592 assert_eq!(page_two.items().len(), 2); 593 assert!(page_two.next_before_sequence().is_none()); 594 assert_eq!( 595 page_two 596 .items() 597 .iter() 598 .filter(|record| record.outcome() == MycAuditOutcome::Succeeded) 599 .count(), 600 1 601 ); 602 assert_eq!( 603 repository 604 .read_audit_page( 605 MycAuditPageLimit::new(1).expect("page limit"), 606 Some(page_one.snapshot_sequence() + 1), 607 None, 608 ) 609 .await 610 .expect_err("future snapshot") 611 .kind(), 612 MycStateRepositoryErrorKind::Binding 613 ); 614 assert_eq!( 615 repository 616 .compact_governance_evidence( 617 time(1_000), 618 MycGovernanceCompactionPolicy::new(499, 1).expect("structural policy"), 619 audit_correlation(0x8d), 620 ) 621 .await 622 .expect_err("retention policy must match bound configuration") 623 .kind(), 624 MycStateRepositoryErrorKind::Binding 625 ); 626 627 host.close() 628 .await 629 .expect("host close before retention pass"); 630 let host = open_myc_state_read_write(&runtime, &metadata, applied_at, &build) 631 .await 632 .expect("reopened host"); 633 let repository = host.repository(); 634 assert_eq!( 635 repository 636 .read_audit_page( 637 MycAuditPageLimit::new(4).expect("page limit"), 638 Some(page_one.snapshot_sequence()), 639 None, 640 ) 641 .await 642 .expect("reopened audit page") 643 .items() 644 .len(), 645 4 646 ); 647 let compacted = repository 648 .compact_governance_evidence( 649 time(1_000), 650 MycGovernanceCompactionPolicy::new(500, MYC_COMPACTION_MAX_ROWS) 651 .expect("compaction policy"), 652 audit_correlation(0x8c), 653 ) 654 .await 655 .expect("bounded compaction"); 656 assert_eq!(compacted.removed_audit_records(), 4); 657 assert_eq!(compacted.removed_rate_subjects(), 2); 658 let replayed_compaction = repository 659 .compact_governance_evidence( 660 time(1_000), 661 MycGovernanceCompactionPolicy::new(500, MYC_COMPACTION_MAX_ROWS) 662 .expect("compaction policy"), 663 audit_correlation(0x8c), 664 ) 665 .await 666 .expect("idempotent compaction replay"); 667 assert_eq!(replayed_compaction.removed_audit_records(), 0); 668 assert_eq!(replayed_compaction.removed_rate_subjects(), 0); 669 let retained = repository 670 .read_audit_page(MycAuditPageLimit::new(2).expect("page limit"), None, None) 671 .await 672 .expect("retained compaction audit"); 673 assert_eq!(retained.items().len(), 1); 674 assert_eq!( 675 retained.items()[0].kind(), 676 MycAuditKind::GovernanceCompaction 677 ); 678 assert_eq!( 679 retained.items()[0].correlation_id(), 680 audit_correlation(0x8c) 681 ); 682 assert!(retained.items()[0].operation_id().is_none()); 683 assert_eq!(retained.items()[0].reason(), MycAuditReasonCode::Compacted); 684 assert!(matches!( 685 repository 686 .admit_connection(accepted_request) 687 .await 688 .expect("authoritative decision replay"), 689 MycConnectionAdmission::ExactReplay(_) 690 )); 691 assert!(matches!( 692 repository 693 .admit_connection(&reset_request) 694 .await 695 .expect("second authoritative decision replay"), 696 MycConnectionAdmission::ExactReplay(_) 697 )); 698 host.close().await.expect("final host close"); 699 700 let options = SqliteConnectOptions::new() 701 .filename(runtime.artifacts().state_database()) 702 .create_if_missing(false) 703 .read_only(true) 704 .disable_statement_logging(); 705 let mut database = sqlx::SqliteConnection::connect_with(&options) 706 .await 707 .expect("inspection connection"); 708 assert_eq!( 709 sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM connections") 710 .fetch_one(&mut database) 711 .await 712 .expect("connection count"), 713 2 714 ); 715 assert_eq!( 716 sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM nip46_request_decisions") 717 .fetch_one(&mut database) 718 .await 719 .expect("decision count"), 720 2 721 ); 722 assert_eq!( 723 sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM operation_audit") 724 .fetch_one(&mut database) 725 .await 726 .expect("audit count"), 727 1 728 ); 729 assert_eq!( 730 sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM connection_rate_windows") 731 .fetch_one(&mut database) 732 .await 733 .expect("rate count"), 734 0 735 ); 736 database.close().await.expect("inspection close"); 737 } 738 739 #[tokio::test] 740 async fn challenge_creation_and_authorization_use_distinct_durable_rate_budgets() { 741 let directory = tempfile::tempdir().expect("temporary root"); 742 let runtime = runtime(directory.path()); 743 prepare_state_directory(&runtime); 744 let configuration = std::str::from_utf8(CONFIG_EXAMPLE) 745 .expect("UTF-8 configuration") 746 .replacen( 747 "[rate_limits.challenge_creation]\nscope = \"connection\"\nwindow_ms = 120000\nmax_attempts = 5\nretention_ms = 86400000\nmaximum_tracked_subjects = 16384", 748 "[rate_limits.challenge_creation]\nscope = \"connection\"\nwindow_ms = 100\nmax_attempts = 2\nretention_ms = 200\nmaximum_tracked_subjects = 8", 749 1, 750 ) 751 .replacen( 752 "[rate_limits.challenge_authorization]\nscope = \"connection\"\nwindow_ms = 120000\nmax_attempts = 5\nretention_ms = 86400000\nmaximum_tracked_subjects = 16384", 753 "[rate_limits.challenge_authorization]\nscope = \"connection\"\nwindow_ms = 100\nmax_attempts = 1\nretention_ms = 200\nmaximum_tracked_subjects = 8", 754 1, 755 ); 756 let metadata = metadata_from_bytes(&runtime, configuration.as_bytes()); 757 let (applied_at, build) = migration_evidence(); 758 initialize_myc_state(&runtime, &metadata, applied_at, &build) 759 .await 760 .expect("state initialization"); 761 let host = open_myc_state_read_write(&runtime, &metadata, applied_at, &build) 762 .await 763 .expect("writable host"); 764 let repository = host.repository(); 765 let connect = admit_request( 766 &repository, 767 trusted_client(), 768 "distinct-rates-connect", 769 0xa0, 770 MycSignerRequestMethod::Connect, 771 0xa1, 772 10, 773 ) 774 .await; 775 let connection = repository 776 .admit_connection(&connection_request( 777 connect, 778 permission_set(&[MycConnectionPermission::Nip44Encrypt]), 779 17, 780 0xa2, 781 11, 782 Some(1_000), 783 MycConnectionAdmissionPolicy::Trusted, 784 )) 785 .await 786 .expect("connection admission") 787 .record() 788 .expect("admission record") 789 .connection() 790 .expect("connection") 791 .clone(); 792 let first_operation = admit_request( 793 &repository, 794 trusted_client(), 795 "distinct-rates-first", 796 0xa3, 797 MycSignerRequestMethod::Ping, 798 0xa4, 799 20, 800 ) 801 .await; 802 let second_operation = admit_request( 803 &repository, 804 trusted_client(), 805 "distinct-rates-second", 806 0xa5, 807 MycSignerRequestMethod::Ping, 808 0xa6, 809 21, 810 ) 811 .await; 812 let first_request = MycAuthorizationChallengeRequest::new( 813 first_operation, 814 connection.id(), 815 policy_generation(17), 816 MycAuthorizationChallengeUrl::new("https://myc.example.test/auth/challenge").expect("URL"), 817 MycAuthorizationChallengeNonce::from_injected_entropy([0xa7; 32]), 818 time(22), 819 time(200), 820 ) 821 .expect("first challenge request"); 822 let second_request = MycAuthorizationChallengeRequest::new( 823 second_operation, 824 connection.id(), 825 policy_generation(17), 826 MycAuthorizationChallengeUrl::new("https://myc.example.test/auth/challenge").expect("URL"), 827 MycAuthorizationChallengeNonce::from_injected_entropy([0xa8; 32]), 828 time(23), 829 time(200), 830 ) 831 .expect("second challenge request"); 832 let first = repository 833 .issue_authorization_challenge(&first_request) 834 .await 835 .expect("first challenge"); 836 let second = repository 837 .issue_authorization_challenge(&second_request) 838 .await 839 .expect("second challenge"); 840 assert!(matches!( 841 first, 842 MycAuthorizationChallengeAdmission::Created(_) 843 )); 844 assert!(matches!( 845 second, 846 MycAuthorizationChallengeAdmission::Created(_) 847 )); 848 let first_id = first.record().expect("first challenge record").id(); 849 let second_id = second.record().expect("second challenge record").id(); 850 assert!( 851 repository 852 .authorize_challenge( 853 first_id, 854 connection.id(), 855 first_operation, 856 policy_generation(17), 857 time(24), 858 ) 859 .await 860 .expect("first authorization") 861 .record() 862 .is_some() 863 ); 864 let limited = repository 865 .authorize_challenge( 866 second_id, 867 connection.id(), 868 second_operation, 869 policy_generation(17), 870 time(25), 871 ) 872 .await 873 .expect("bounded authorization"); 874 assert!(limited.record().is_none()); 875 assert!(format!("{limited:?}").contains("RateLimited")); 876 assert!( 877 repository 878 .authorize_challenge( 879 second_id, 880 connection.id(), 881 second_operation, 882 policy_generation(17), 883 time(125), 884 ) 885 .await 886 .expect("stable authorization rejection") 887 .record() 888 .is_none() 889 ); 890 host.close().await.expect("host close"); 891 let host = open_myc_state_read_write(&runtime, &metadata, applied_at, &build) 892 .await 893 .expect("reopened host"); 894 assert!( 895 host.repository() 896 .authorize_challenge( 897 second_id, 898 connection.id(), 899 second_operation, 900 policy_generation(17), 901 time(225), 902 ) 903 .await 904 .expect("reopened stable rejection") 905 .record() 906 .is_none() 907 ); 908 host.close().await.expect("final close"); 909 } 910 911 #[tokio::test] 912 async fn connection_admission_and_operator_decisions_are_atomic_replay_safe_and_denial_direct() { 913 let directory = tempfile::tempdir().expect("temporary root"); 914 let runtime = runtime(directory.path()); 915 prepare_state_directory(&runtime); 916 let metadata = metadata(&runtime); 917 let (applied_at, build) = migration_evidence(); 918 initialize_myc_state(&runtime, &metadata, applied_at, &build) 919 .await 920 .expect("state initialization"); 921 let host = open_myc_state_read_write(&runtime, &metadata, applied_at, &build) 922 .await 923 .expect("writable host"); 924 let repository = host.repository(); 925 926 let trusted_operation = admit_request( 927 &repository, 928 trusted_client(), 929 "connect-trusted", 930 0x10, 931 MycSignerRequestMethod::Connect, 932 0x11, 933 100, 934 ) 935 .await; 936 let requested = permission_set(&[ 937 MycConnectionPermission::Nip44Encrypt, 938 MycConnectionPermission::SignEvent(1), 939 ]); 940 let forged_unknown_policy = MycConnectionAdmissionRequest::new( 941 trusted_operation, 942 trusted_client(), 943 requested.clone(), 944 policy_generation(1), 945 MycConnectionNonce::from_injected_entropy([0x1e; 32]), 946 time(110), 947 None, 948 MycConnectionAdmissionPolicy::ExplicitApproval, 949 MycRateRelayId::new("primary").expect("relay ID"), 950 ) 951 .expect("structurally valid forged policy"); 952 assert_eq!( 953 repository 954 .admit_connection(&forged_unknown_policy) 955 .await 956 .expect_err("configuration decides trusted admission") 957 .kind(), 958 MycStateRepositoryErrorKind::Binding 959 ); 960 let above_ceiling = MycConnectionAdmissionRequest::new( 961 trusted_operation, 962 trusted_client(), 963 permission_set(&[MycConnectionPermission::Ping]), 964 policy_generation(1), 965 MycConnectionNonce::from_injected_entropy([0x1d; 32]), 966 time(110), 967 Some(time(1_000)), 968 MycConnectionAdmissionPolicy::Trusted, 969 MycRateRelayId::new("primary").expect("relay ID"), 970 ) 971 .expect("structurally valid permission expansion"); 972 assert_eq!( 973 repository 974 .admit_connection(&above_ceiling) 975 .await 976 .expect_err("configuration permission ceiling") 977 .kind(), 978 MycStateRepositoryErrorKind::Binding 979 ); 980 assert_eq!( 981 repository 982 .admit_connection(&connection_request( 983 trusted_operation, 984 requested.clone(), 985 1, 986 0x1f, 987 99, 988 Some(1_000), 989 MycConnectionAdmissionPolicy::Trusted, 990 )) 991 .await 992 .expect_err("connection admission cannot predate the request") 993 .kind(), 994 MycStateRepositoryErrorKind::Binding 995 ); 996 let trusted = repository 997 .admit_connection(&connection_request( 998 trusted_operation, 999 requested.clone(), 1000 1, 1001 0x20, 1002 110, 1003 Some(1_000), 1004 MycConnectionAdmissionPolicy::Trusted, 1005 )) 1006 .await 1007 .expect("trusted admission"); 1008 assert!(matches!(trusted, MycConnectionAdmission::Admitted(_))); 1009 assert_eq!( 1010 trusted.record().expect("admission record").decision(), 1011 myc::MycConnectionDecision::Allowed 1012 ); 1013 let trusted_connection = trusted 1014 .record() 1015 .expect("admission record") 1016 .connection() 1017 .expect("connection"); 1018 assert_eq!(trusted_connection.status(), MycConnectionStatus::Active); 1019 assert_eq!(trusted_connection.granted_permissions(), &requested); 1020 let trusted_id = trusted_connection.id(); 1021 assert_eq!( 1022 hex(trusted_id.as_bytes()), 1023 "08a11316dac6cf56849f1b7e6e9a50ea4b3e577ee39ea18440c33a1c2ec22671" 1024 ); 1025 let trusted_replay = repository 1026 .admit_connection(&connection_request( 1027 trusted_operation, 1028 requested.clone(), 1029 1, 1030 0x21, 1031 111, 1032 Some(1_000), 1033 MycConnectionAdmissionPolicy::Trusted, 1034 )) 1035 .await 1036 .expect("trusted replay"); 1037 assert!(matches!( 1038 trusted_replay, 1039 MycConnectionAdmission::ExactReplay(_) 1040 )); 1041 assert_eq!( 1042 trusted_replay 1043 .record() 1044 .expect("admission record") 1045 .connection() 1046 .expect("connection") 1047 .id(), 1048 trusted_id 1049 ); 1050 1051 let pending_operation = admit_request( 1052 &repository, 1053 client(), 1054 "connect-pending", 1055 0x12, 1056 MycSignerRequestMethod::Connect, 1057 0x13, 1058 120, 1059 ) 1060 .await; 1061 let pending = repository 1062 .admit_connection(&connection_request( 1063 pending_operation, 1064 requested.clone(), 1065 2, 1066 0x22, 1067 121, 1068 None, 1069 MycConnectionAdmissionPolicy::ExplicitApproval, 1070 )) 1071 .await 1072 .expect("pending admission"); 1073 assert_eq!( 1074 pending.record().expect("admission record").decision(), 1075 myc::MycConnectionDecision::PendingApproval 1076 ); 1077 let pending_id = pending 1078 .record() 1079 .expect("admission record") 1080 .connection() 1081 .expect("pending connection") 1082 .id(); 1083 let granted = permission_set(&[MycConnectionPermission::Nip44Encrypt]); 1084 assert_eq!( 1085 repository 1086 .decide_pending_connection( 1087 pending_operation, 1088 pending_id, 1089 policy_generation(2), 1090 time(130), 1091 audit_correlation(0x6f), 1092 MycConnectionOperatorDecision::Approve { 1093 granted_permissions: permission_set(&[MycConnectionPermission::Ping]), 1094 authorized_until: Some(time(900)), 1095 }, 1096 ) 1097 .await 1098 .expect_err("operator cannot expand the configured ceiling") 1099 .kind(), 1100 MycStateRepositoryErrorKind::Binding 1101 ); 1102 assert_eq!( 1103 repository 1104 .decide_pending_connection( 1105 pending_operation, 1106 pending_id, 1107 policy_generation(2), 1108 time(120), 1109 audit_correlation(0x70), 1110 MycConnectionOperatorDecision::Approve { 1111 granted_permissions: granted.clone(), 1112 authorized_until: Some(time(900)), 1113 }, 1114 ) 1115 .await 1116 .expect_err("operator decision cannot predate pending state") 1117 .kind(), 1118 MycStateRepositoryErrorKind::Binding 1119 ); 1120 let approved = repository 1121 .decide_pending_connection( 1122 pending_operation, 1123 pending_id, 1124 policy_generation(2), 1125 time(130), 1126 audit_correlation(0x71), 1127 MycConnectionOperatorDecision::Approve { 1128 granted_permissions: granted.clone(), 1129 authorized_until: Some(time(900)), 1130 }, 1131 ) 1132 .await 1133 .expect("operator approval"); 1134 assert_eq!(approved.status(), MycConnectionStatus::Active); 1135 assert_eq!(approved.granted_permissions(), &granted); 1136 let approval_replay = repository 1137 .decide_pending_connection( 1138 pending_operation, 1139 pending_id, 1140 policy_generation(2), 1141 time(131), 1142 audit_correlation(0x71), 1143 MycConnectionOperatorDecision::Approve { 1144 granted_permissions: granted.clone(), 1145 authorized_until: Some(time(900)), 1146 }, 1147 ) 1148 .await 1149 .expect("approval replay"); 1150 assert_eq!(approval_replay, approved); 1151 let admission_after_approval = repository 1152 .admit_connection(&connection_request( 1153 pending_operation, 1154 requested.clone(), 1155 2, 1156 0x23, 1157 132, 1158 None, 1159 MycConnectionAdmissionPolicy::ExplicitApproval, 1160 )) 1161 .await 1162 .expect("admission replay after approval"); 1163 assert!(matches!( 1164 admission_after_approval, 1165 MycConnectionAdmission::ExactReplay(_) 1166 )); 1167 assert_eq!( 1168 admission_after_approval 1169 .record() 1170 .expect("admission record") 1171 .decision(), 1172 myc::MycConnectionDecision::Allowed 1173 ); 1174 1175 let operator_denied_operation = admit_request( 1176 &repository, 1177 client(), 1178 "connect-operator-denied", 1179 0x16, 1180 MycSignerRequestMethod::Connect, 1181 0x17, 1182 135, 1183 ) 1184 .await; 1185 let operator_pending = repository 1186 .admit_connection(&connection_request( 1187 operator_denied_operation, 1188 requested.clone(), 1189 2, 1190 0x27, 1191 136, 1192 None, 1193 MycConnectionAdmissionPolicy::ExplicitApproval, 1194 )) 1195 .await 1196 .expect("operator-denied pending admission"); 1197 let operator_denied_id = operator_pending 1198 .record() 1199 .expect("admission record") 1200 .connection() 1201 .expect("operator-denied connection") 1202 .id(); 1203 let operator_denied = repository 1204 .decide_pending_connection( 1205 operator_denied_operation, 1206 operator_denied_id, 1207 policy_generation(2), 1208 time(137), 1209 audit_correlation(0x72), 1210 MycConnectionOperatorDecision::Deny, 1211 ) 1212 .await 1213 .expect("operator denial"); 1214 assert_eq!(operator_denied.status(), MycConnectionStatus::Denied); 1215 assert_eq!( 1216 repository 1217 .decide_pending_connection( 1218 operator_denied_operation, 1219 operator_denied_id, 1220 policy_generation(2), 1221 time(138), 1222 audit_correlation(0x72), 1223 MycConnectionOperatorDecision::Deny, 1224 ) 1225 .await 1226 .expect("operator denial replay"), 1227 operator_denied 1228 ); 1229 1230 let denied_operation = admit_request( 1231 &repository, 1232 denied_client(), 1233 "connect-denied", 1234 0x14, 1235 MycSignerRequestMethod::Connect, 1236 0x15, 1237 140, 1238 ) 1239 .await; 1240 let denied = repository 1241 .admit_connection(&connection_request( 1242 denied_operation, 1243 permission_set(&[MycConnectionPermission::Ping]), 1244 3, 1245 0x24, 1246 141, 1247 None, 1248 MycConnectionAdmissionPolicy::Denied, 1249 )) 1250 .await 1251 .expect("direct denial"); 1252 assert_eq!( 1253 denied.record().expect("admission record").decision(), 1254 myc::MycConnectionDecision::Denied 1255 ); 1256 assert!( 1257 denied 1258 .record() 1259 .expect("admission record") 1260 .connection() 1261 .is_none() 1262 ); 1263 let denied_replay = repository 1264 .admit_connection(&connection_request( 1265 denied_operation, 1266 permission_set(&[MycConnectionPermission::Ping]), 1267 3, 1268 0x25, 1269 142, 1270 None, 1271 MycConnectionAdmissionPolicy::Denied, 1272 )) 1273 .await 1274 .expect("denial replay"); 1275 assert!(matches!( 1276 denied_replay, 1277 MycConnectionAdmission::ExactReplay(_) 1278 )); 1279 assert!( 1280 denied_replay 1281 .record() 1282 .expect("admission record") 1283 .connection() 1284 .is_none() 1285 ); 1286 1287 let mismatch = repository 1288 .admit_connection(&connection_request( 1289 denied_operation, 1290 permission_set(&[MycConnectionPermission::Nip44Encrypt]), 1291 3, 1292 0x26, 1293 143, 1294 None, 1295 MycConnectionAdmissionPolicy::ExplicitApproval, 1296 )) 1297 .await 1298 .expect_err("policy mismatch"); 1299 assert_eq!(mismatch.kind(), MycStateRepositoryErrorKind::Binding); 1300 1301 host.close().await.expect("host close"); 1302 let options = SqliteConnectOptions::new() 1303 .filename(runtime.artifacts().state_database()) 1304 .create_if_missing(false) 1305 .read_only(true) 1306 .disable_statement_logging(); 1307 let mut connection = sqlx::SqliteConnection::connect_with(&options) 1308 .await 1309 .expect("inspection connection"); 1310 assert_eq!( 1311 sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM connections") 1312 .fetch_one(&mut connection) 1313 .await 1314 .expect("connection count"), 1315 3 1316 ); 1317 assert_eq!( 1318 sqlx::query_scalar::<_, i64>( 1319 "SELECT COUNT(*) FROM nip46_request_decisions WHERE reason_code = 'policy_denied' AND connection_id IS NULL" 1320 ) 1321 .fetch_one(&mut connection) 1322 .await 1323 .expect("direct-denial count"), 1324 1 1325 ); 1326 connection.close().await.expect("inspection close"); 1327 } 1328 1329 #[tokio::test] 1330 async fn configured_denial_precedes_saturated_unknown_client_rate_windows() { 1331 let directory = tempfile::tempdir().expect("temporary root"); 1332 let runtime = runtime(directory.path()); 1333 prepare_state_directory(&runtime); 1334 let configuration = std::str::from_utf8(CONFIG_EXAMPLE) 1335 .expect("UTF-8 configuration") 1336 .replacen( 1337 "[rate_limits.connection_admission]\nscope = \"global_and_relay\"\nwindow_ms = 60000\nmax_attempts = 10\nretention_ms = 3600000\nmaximum_tracked_subjects = 4096", 1338 "[rate_limits.connection_admission]\nscope = \"global_and_relay\"\nwindow_ms = 100\nmax_attempts = 1\nretention_ms = 200\nmaximum_tracked_subjects = 8", 1339 1, 1340 ); 1341 let metadata = metadata_from_bytes(&runtime, configuration.as_bytes()); 1342 let (applied_at, build) = migration_evidence(); 1343 initialize_myc_state(&runtime, &metadata, applied_at, &build) 1344 .await 1345 .expect("state initialization"); 1346 let host = open_myc_state_read_write(&runtime, &metadata, applied_at, &build) 1347 .await 1348 .expect("writable host"); 1349 let repository = host.repository(); 1350 1351 let first = admit_request( 1352 &repository, 1353 client(), 1354 "unknown-first", 1355 0xb0, 1356 MycSignerRequestMethod::Connect, 1357 0xb1, 1358 100, 1359 ) 1360 .await; 1361 let second = admit_request( 1362 &repository, 1363 client(), 1364 "unknown-second", 1365 0xb2, 1366 MycSignerRequestMethod::Connect, 1367 0xb3, 1368 100, 1369 ) 1370 .await; 1371 let denied = admit_request( 1372 &repository, 1373 denied_client(), 1374 "configured-denial", 1375 0xb4, 1376 MycSignerRequestMethod::Connect, 1377 0xb5, 1378 100, 1379 ) 1380 .await; 1381 let trusted_first = admit_request( 1382 &repository, 1383 trusted_client(), 1384 "trusted-first", 1385 0xb9, 1386 MycSignerRequestMethod::Connect, 1387 0xba, 1388 202, 1389 ) 1390 .await; 1391 let trusted_second = admit_request( 1392 &repository, 1393 trusted_client(), 1394 "trusted-second", 1395 0xbb, 1396 MycSignerRequestMethod::Connect, 1397 0xbc, 1398 202, 1399 ) 1400 .await; 1401 let permissions = permission_set(&[MycConnectionPermission::Nip44Encrypt]); 1402 assert!(matches!( 1403 repository 1404 .admit_connection(&connection_request( 1405 first, 1406 permissions.clone(), 1407 1, 1408 0xb6, 1409 101, 1410 None, 1411 MycConnectionAdmissionPolicy::ExplicitApproval, 1412 )) 1413 .await 1414 .expect("first unknown admission"), 1415 MycConnectionAdmission::Admitted(_) 1416 )); 1417 assert!(matches!( 1418 repository 1419 .admit_connection(&connection_request( 1420 second, 1421 permissions, 1422 1, 1423 0xb7, 1424 101, 1425 None, 1426 MycConnectionAdmissionPolicy::ExplicitApproval, 1427 )) 1428 .await 1429 .expect("saturated unknown admission"), 1430 MycConnectionAdmission::RateLimited 1431 )); 1432 assert!(matches!( 1433 repository 1434 .admit_connection(&connection_request( 1435 trusted_first, 1436 permission_set(&[MycConnectionPermission::Nip44Encrypt]), 1437 1, 1438 0xbd, 1439 202, 1440 Some(1_000), 1441 MycConnectionAdmissionPolicy::Trusted, 1442 )) 1443 .await 1444 .expect("first trusted admission in the next window"), 1445 MycConnectionAdmission::Admitted(_) 1446 )); 1447 assert!(matches!( 1448 repository 1449 .admit_connection(&connection_request( 1450 trusted_second, 1451 permission_set(&[MycConnectionPermission::Nip44Encrypt]), 1452 1, 1453 0xbe, 1454 202, 1455 Some(1_000), 1456 MycConnectionAdmissionPolicy::Trusted, 1457 )) 1458 .await 1459 .expect("saturated trusted admission"), 1460 MycConnectionAdmission::RateLimited 1461 )); 1462 let direct = repository 1463 .admit_connection(&connection_request( 1464 denied, 1465 permission_set(&[MycConnectionPermission::Ping]), 1466 1, 1467 0xb8, 1468 101, 1469 None, 1470 MycConnectionAdmissionPolicy::Denied, 1471 )) 1472 .await 1473 .expect("configured denial bypasses unknown-client rate admission"); 1474 assert_eq!( 1475 direct.record().expect("denial record").decision(), 1476 myc::MycConnectionDecision::Denied 1477 ); 1478 assert!( 1479 direct 1480 .record() 1481 .expect("denial record") 1482 .connection() 1483 .is_none() 1484 ); 1485 host.close().await.expect("host close"); 1486 } 1487 1488 #[tokio::test] 1489 async fn challenge_authorization_expiry_and_terminal_replay_remain_exactly_bound() { 1490 let directory = tempfile::tempdir().expect("temporary root"); 1491 let runtime = runtime(directory.path()); 1492 prepare_state_directory(&runtime); 1493 let metadata = metadata(&runtime); 1494 let (applied_at, build) = migration_evidence(); 1495 initialize_myc_state(&runtime, &metadata, applied_at, &build) 1496 .await 1497 .expect("state initialization"); 1498 let host = open_myc_state_read_write(&runtime, &metadata, applied_at, &build) 1499 .await 1500 .expect("writable host"); 1501 let repository = host.repository(); 1502 1503 let connect_operation = admit_request( 1504 &repository, 1505 trusted_client(), 1506 "connect-challenge", 1507 0x30, 1508 MycSignerRequestMethod::Connect, 1509 0x31, 1510 200, 1511 ) 1512 .await; 1513 let connection = repository 1514 .admit_connection(&connection_request( 1515 connect_operation, 1516 permission_set(&[MycConnectionPermission::Nip44Encrypt]), 1517 7, 1518 0x32, 1519 201, 1520 Some(500), 1521 MycConnectionAdmissionPolicy::Trusted, 1522 )) 1523 .await 1524 .expect("connection") 1525 .record() 1526 .expect("admission record") 1527 .connection() 1528 .expect("active connection") 1529 .clone(); 1530 1531 let ping_operation = admit_request( 1532 &repository, 1533 trusted_client(), 1534 "ping-challenge", 1535 0x33, 1536 MycSignerRequestMethod::Ping, 1537 0x34, 1538 210, 1539 ) 1540 .await; 1541 let invalid_lifetime = MycAuthorizationChallengeRequest::new( 1542 ping_operation, 1543 connection.id(), 1544 policy_generation(7), 1545 MycAuthorizationChallengeUrl::new("https://myc.example.test/auth/challenge").expect("URL"), 1546 MycAuthorizationChallengeNonce::from_injected_entropy([0x34; 32]), 1547 time(211), 1548 time(211), 1549 ) 1550 .expect_err("invalid lifetime"); 1551 assert_eq!( 1552 invalid_lifetime.kind(), 1553 MycConnectionStateErrorKind::InvalidChallengeLifetime 1554 ); 1555 let client_selected_url = MycAuthorizationChallengeRequest::new( 1556 ping_operation, 1557 connection.id(), 1558 policy_generation(7), 1559 MycAuthorizationChallengeUrl::new("https://attacker.example/redirect").expect("URL"), 1560 MycAuthorizationChallengeNonce::from_injected_entropy([0x34; 32]), 1561 time(211), 1562 time(300), 1563 ) 1564 .expect("structurally valid client-selected URL"); 1565 assert_eq!( 1566 repository 1567 .issue_authorization_challenge(&client_selected_url) 1568 .await 1569 .expect_err("only configured operator URL is authoritative") 1570 .kind(), 1571 MycStateRepositoryErrorKind::Binding 1572 ); 1573 let excessive_pending_lifetime = MycAuthorizationChallengeRequest::new( 1574 ping_operation, 1575 connection.id(), 1576 policy_generation(7), 1577 MycAuthorizationChallengeUrl::new("https://myc.example.test/auth/challenge").expect("URL"), 1578 MycAuthorizationChallengeNonce::from_injected_entropy([0x34; 32]), 1579 time(211), 1580 time(900_212), 1581 ) 1582 .expect("structurally valid excessive pending lifetime"); 1583 assert_eq!( 1584 repository 1585 .issue_authorization_challenge(&excessive_pending_lifetime) 1586 .await 1587 .expect_err("configured pending lifetime is authoritative") 1588 .kind(), 1589 MycStateRepositoryErrorKind::Binding 1590 ); 1591 let challenge_request = MycAuthorizationChallengeRequest::new( 1592 ping_operation, 1593 connection.id(), 1594 policy_generation(7), 1595 MycAuthorizationChallengeUrl::new("https://myc.example.test/auth/challenge").expect("URL"), 1596 MycAuthorizationChallengeNonce::from_injected_entropy([0x35; 32]), 1597 time(211), 1598 time(300), 1599 ) 1600 .expect("challenge request"); 1601 let premature_request = MycAuthorizationChallengeRequest::new( 1602 ping_operation, 1603 connection.id(), 1604 policy_generation(7), 1605 MycAuthorizationChallengeUrl::new("https://myc.example.test/auth/challenge").expect("URL"), 1606 MycAuthorizationChallengeNonce::from_injected_entropy([0x34; 32]), 1607 time(209), 1608 time(300), 1609 ) 1610 .expect("structurally valid premature request"); 1611 assert_eq!( 1612 repository 1613 .issue_authorization_challenge(&premature_request) 1614 .await 1615 .expect_err("challenge cannot predate request admission") 1616 .kind(), 1617 MycStateRepositoryErrorKind::Binding 1618 ); 1619 let challenge = repository 1620 .issue_authorization_challenge(&challenge_request) 1621 .await 1622 .expect("challenge"); 1623 assert!(matches!( 1624 challenge, 1625 MycAuthorizationChallengeAdmission::Created(_) 1626 )); 1627 assert_eq!( 1628 challenge.record().expect("challenge record").state(), 1629 MycAuthorizationChallengeState::Pending 1630 ); 1631 let challenge_id = challenge.record().expect("challenge record").id(); 1632 assert_eq!( 1633 hex(challenge_id.as_bytes()), 1634 "9a85ce42301af50287626ddcf209a145a2742cf0ef178e44acf06108d1b86b29" 1635 ); 1636 1637 let replay_request = MycAuthorizationChallengeRequest::new( 1638 ping_operation, 1639 connection.id(), 1640 policy_generation(7), 1641 MycAuthorizationChallengeUrl::new("https://myc.example.test/auth/challenge").expect("URL"), 1642 MycAuthorizationChallengeNonce::from_injected_entropy([0x36; 32]), 1643 time(211), 1644 time(300), 1645 ) 1646 .expect("replay request"); 1647 let replay = repository 1648 .issue_authorization_challenge(&replay_request) 1649 .await 1650 .expect("challenge replay"); 1651 assert!(matches!( 1652 replay, 1653 MycAuthorizationChallengeAdmission::ExactReplay(_) 1654 )); 1655 assert_eq!( 1656 replay.record().expect("challenge record").id(), 1657 challenge_id 1658 ); 1659 1660 assert_eq!( 1661 repository 1662 .authorize_challenge( 1663 challenge_id, 1664 connection.id(), 1665 ping_operation, 1666 policy_generation(7), 1667 time(210), 1668 ) 1669 .await 1670 .expect_err("resolution cannot predate challenge issue") 1671 .kind(), 1672 MycStateRepositoryErrorKind::Binding 1673 ); 1674 1675 let authorized = repository 1676 .authorize_challenge( 1677 challenge_id, 1678 connection.id(), 1679 ping_operation, 1680 policy_generation(7), 1681 time(300), 1682 ) 1683 .await 1684 .expect("authorization at exact deadline"); 1685 assert_eq!( 1686 authorized.record().expect("authorization record").state(), 1687 MycAuthorizationChallengeState::Authorized 1688 ); 1689 assert_eq!( 1690 authorized 1691 .record() 1692 .expect("authorization record") 1693 .resolved_at(), 1694 Some(time(300)) 1695 ); 1696 let terminal_replay = repository 1697 .authorize_challenge( 1698 challenge_id, 1699 connection.id(), 1700 ping_operation, 1701 policy_generation(7), 1702 time(400), 1703 ) 1704 .await 1705 .expect("terminal replay"); 1706 assert_eq!( 1707 terminal_replay.record().expect("authorization record"), 1708 authorized.record().expect("authorization record") 1709 ); 1710 assert_eq!( 1711 repository 1712 .authorize_challenge( 1713 challenge_id, 1714 connection.id(), 1715 ping_operation, 1716 policy_generation(7), 1717 time(3_600_301), 1718 ) 1719 .await 1720 .expect_err("authorized challenge lifetime is bounded by configuration") 1721 .kind(), 1722 MycStateRepositoryErrorKind::Binding 1723 ); 1724 1725 let deadline_operation = admit_request( 1726 &repository, 1727 trusted_client(), 1728 "ping-deadline", 1729 0x3a, 1730 MycSignerRequestMethod::Ping, 1731 0x3b, 1732 410, 1733 ) 1734 .await; 1735 let deadline_request = MycAuthorizationChallengeRequest::new( 1736 deadline_operation, 1737 connection.id(), 1738 policy_generation(7), 1739 MycAuthorizationChallengeUrl::new("https://myc.example.test/auth/challenge").expect("URL"), 1740 MycAuthorizationChallengeNonce::from_injected_entropy([0x3c; 32]), 1741 time(416), 1742 time(440), 1743 ) 1744 .expect("deadline request"); 1745 let deadline_challenge = repository 1746 .issue_authorization_challenge(&deadline_request) 1747 .await 1748 .expect("deadline challenge"); 1749 let deadline_expired = repository 1750 .authorize_challenge( 1751 deadline_challenge.record().expect("challenge record").id(), 1752 connection.id(), 1753 deadline_operation, 1754 policy_generation(7), 1755 time(441), 1756 ) 1757 .await 1758 .expect("challenge expiry"); 1759 assert_eq!( 1760 deadline_expired 1761 .record() 1762 .expect("authorization record") 1763 .state(), 1764 MycAuthorizationChallengeState::Expired 1765 ); 1766 1767 let expiring_operation = admit_request( 1768 &repository, 1769 trusted_client(), 1770 "ping-expiring", 1771 0x37, 1772 MycSignerRequestMethod::Ping, 1773 0x38, 1774 450, 1775 ) 1776 .await; 1777 let expiring_request = MycAuthorizationChallengeRequest::new( 1778 expiring_operation, 1779 connection.id(), 1780 policy_generation(7), 1781 MycAuthorizationChallengeUrl::new("https://myc.example.test/auth/challenge").expect("URL"), 1782 MycAuthorizationChallengeNonce::from_injected_entropy([0x39; 32]), 1783 time(451), 1784 time(600), 1785 ) 1786 .expect("expiring request"); 1787 let expiring = repository 1788 .issue_authorization_challenge(&expiring_request) 1789 .await 1790 .expect("expiring challenge"); 1791 let expired = repository 1792 .authorize_challenge( 1793 expiring.record().expect("challenge record").id(), 1794 connection.id(), 1795 expiring_operation, 1796 policy_generation(7), 1797 time(501), 1798 ) 1799 .await 1800 .expect("connection-expired challenge"); 1801 assert_eq!( 1802 expired.record().expect("authorization record").state(), 1803 MycAuthorizationChallengeState::Expired 1804 ); 1805 1806 let expired_connection = repository 1807 .expire_connection( 1808 connection.id(), 1809 policy_generation(7), 1810 time(501), 1811 audit_correlation(0x73), 1812 ) 1813 .await 1814 .expect("connection expiry"); 1815 assert_eq!(expired_connection.status(), MycConnectionStatus::Expired); 1816 assert_eq!( 1817 repository 1818 .expire_connection( 1819 connection.id(), 1820 policy_generation(7), 1821 time(502), 1822 audit_correlation(0x73), 1823 ) 1824 .await 1825 .expect("expiry replay"), 1826 expired_connection 1827 ); 1828 let challenge_replay_after_connection_expiry = repository 1829 .issue_authorization_challenge(&replay_request) 1830 .await 1831 .expect("challenge replay after connection expiry"); 1832 assert!(matches!( 1833 challenge_replay_after_connection_expiry, 1834 MycAuthorizationChallengeAdmission::ExactReplay(_) 1835 )); 1836 assert_eq!( 1837 challenge_replay_after_connection_expiry 1838 .record() 1839 .expect("challenge record"), 1840 authorized.record().expect("authorization record") 1841 ); 1842 1843 let wrong_binding = repository 1844 .authorize_challenge( 1845 challenge_id, 1846 connection.id(), 1847 ping_operation, 1848 policy_generation(8), 1849 time(400), 1850 ) 1851 .await 1852 .expect_err("wrong policy binding"); 1853 assert_eq!(wrong_binding.kind(), MycStateRepositoryErrorKind::Binding); 1854 let rendered = format!( 1855 "{challenge:?} {:?} {wrong_binding} {wrong_binding:?}", 1856 challenge.record().expect("challenge record") 1857 ); 1858 for secret in [ 1859 "operator.example", 1860 "authorize", 1861 CLIENT_PUBLIC_KEY, 1862 "b0f43d00", 1863 ] { 1864 assert!(!rendered.contains(secret)); 1865 } 1866 assert!(Error::source(&wrong_binding).is_none()); 1867 1868 host.close().await.expect("host close"); 1869 let host = open_myc_state_read_write(&runtime, &metadata, applied_at, &build) 1870 .await 1871 .expect("reopened host"); 1872 let replay = host 1873 .repository() 1874 .authorize_challenge( 1875 challenge_id, 1876 connection.id(), 1877 ping_operation, 1878 policy_generation(7), 1879 time(700), 1880 ) 1881 .await 1882 .expect("restart replay"); 1883 assert_eq!( 1884 replay.record().expect("authorization record"), 1885 authorized.record().expect("authorization record") 1886 ); 1887 host.close().await.expect("final close"); 1888 } 1889 1890 #[test] 1891 fn connection_state_source_has_no_ambient_or_external_authority() { 1892 for forbidden in [ 1893 "rand::", 1894 "getrandom", 1895 "std::time", 1896 "SystemTime", 1897 "tokio::spawn", 1898 "spawn_blocking", 1899 "SqlitePool", 1900 "SqliteConnection", 1901 "nostr_sdk", 1902 "reqwest", 1903 "relay::", 1904 "provider::", 1905 ] { 1906 assert!( 1907 !CONNECTION_SOURCE.contains(forbidden), 1908 "found forbidden connection-state authority `{forbidden}`" 1909 ); 1910 assert!( 1911 !GOVERNANCE_SOURCE.contains(forbidden), 1912 "found forbidden governance-state authority `{forbidden}`" 1913 ); 1914 } 1915 for required in [ 1916 "ServiceSqliteTransaction", 1917 "from_injected_entropy", 1918 "policy_denied", 1919 "authorization_challenge_expired", 1920 "LIMIT 65", 1921 ] { 1922 assert!( 1923 CONNECTION_SOURCE.contains(required), 1924 "missing governed connection-state boundary `{required}`" 1925 ); 1926 } 1927 assert!(GOVERNANCE_SOURCE.contains("ServiceSqliteTransaction")); 1928 assert!(GOVERNANCE_SOURCE.contains("LIMIT ?")); 1929 assert!(!GOVERNANCE_SOURCE.contains("SELECT *")); 1930 }