myc

Self-custodial remote signer for Radroots apps
git clone https://radroots.dev/git/myc.git
Log | Files | Refs | README | LICENSE

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 }