myc

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

services_hardening_config_contract.rs (29072B)


      1 #![forbid(unsafe_code)]
      2 
      3 use std::collections::{BTreeMap, BTreeSet};
      4 use std::net::SocketAddr;
      5 
      6 use nostr::PublicKey;
      7 use serde_json::{Map, Value, json};
      8 
      9 const CONFIG_SCHEMA: &str = include_str!("../contracts/services_hardening/config.v1.schema.json");
     10 const CONFIG_EXAMPLE: &str = include_str!("../contracts/services_hardening/config.v1.example.toml");
     11 
     12 #[derive(Clone, Copy)]
     13 enum Profile {
     14     Production,
     15     RepoLocal,
     16 }
     17 
     18 fn schema() -> Value {
     19     serde_json::from_str(CONFIG_SCHEMA).expect("configuration schema must be valid JSON")
     20 }
     21 
     22 fn example() -> Value {
     23     let value = toml::from_str::<toml::Value>(CONFIG_EXAMPLE)
     24         .expect("configuration example must be valid TOML");
     25     serde_json::to_value(value).expect("TOML value must convert to JSON")
     26 }
     27 
     28 fn schema_valid(value: &Value) -> bool {
     29     jsonschema::validator_for(&schema())
     30         .expect("configuration schema must compile")
     31         .is_valid(value)
     32 }
     33 
     34 fn string_array<'a>(value: &'a Value, pointer: &str) -> Vec<&'a str> {
     35     value
     36         .pointer(pointer)
     37         .and_then(Value::as_array)
     38         .expect("string array")
     39         .iter()
     40         .map(|value| value.as_str().expect("string item"))
     41         .collect()
     42 }
     43 
     44 fn semantic_valid(value: &Value, profile: Profile) -> bool {
     45     if !schema_valid(value) {
     46         return false;
     47     }
     48 
     49     let relays = value["relays"].as_array().expect("relays");
     50     let relay_ids = relays
     51         .iter()
     52         .map(|relay| relay["id"].as_str().expect("relay id"))
     53         .collect::<Vec<_>>();
     54     let relay_urls = relays
     55         .iter()
     56         .map(|relay| relay["url"].as_str().expect("relay URL"))
     57         .collect::<Vec<_>>();
     58     let parsed_relay_urls = relay_urls
     59         .iter()
     60         .map(|raw| {
     61             url::Url::parse(raw)
     62                 .ok()
     63                 .filter(|parsed| parsed.as_str() == *raw)
     64         })
     65         .collect::<Vec<_>>();
     66     if relay_ids.iter().collect::<BTreeSet<_>>().len() != relay_ids.len()
     67         || relay_urls.iter().collect::<BTreeSet<_>>().len() != relay_urls.len()
     68         || parsed_relay_urls.iter().any(Option::is_none)
     69         || parsed_relay_urls.iter().flatten().any(|url| {
     70             !url.username().is_empty() || url.password().is_some() || url.fragment().is_some()
     71         })
     72         || relays.iter().any(|relay| {
     73             let read = relay["read"].as_bool().expect("read");
     74             let write = relay["write"].as_bool().expect("write");
     75             (!read && !write) || (relay["required"].as_bool().expect("required") && !read && !write)
     76         })
     77         || !relays
     78             .iter()
     79             .any(|relay| relay["read"].as_bool() == Some(true))
     80         || !relays
     81             .iter()
     82             .any(|relay| relay["write"].as_bool() == Some(true))
     83         || !relays.iter().any(|relay| {
     84             relay["required"].as_bool() == Some(true) && relay["read"].as_bool() == Some(true)
     85         })
     86         || !relays.iter().any(|relay| {
     87             relay["required"].as_bool() == Some(true) && relay["write"].as_bool() == Some(true)
     88         })
     89     {
     90         return false;
     91     }
     92     if parsed_relay_urls.iter().flatten().any(|url| match profile {
     93         Profile::Production => url.scheme() != "wss",
     94         Profile::RepoLocal => match url.scheme() {
     95             "wss" => false,
     96             "ws" => !matches!(
     97                 url.host_str(),
     98                 Some("127.0.0.1" | "localhost" | "[::1]" | "::1")
     99             ),
    100             _ => true,
    101         },
    102     }) {
    103         return false;
    104     }
    105 
    106     let trusted = string_array(value, "/policy/trusted_clients")
    107         .into_iter()
    108         .collect::<BTreeSet<_>>();
    109     let denied = string_array(value, "/policy/denied_clients")
    110         .into_iter()
    111         .collect::<BTreeSet<_>>();
    112     if !trusted.is_disjoint(&denied) {
    113         return false;
    114     }
    115 
    116     let mut expected_role_keys = vec![
    117         value["identity"]["transport"]["expected_public_key"]
    118             .as_str()
    119             .expect("transport key"),
    120         value["identity"]["user"]["expected_public_key"]
    121             .as_str()
    122             .expect("user key"),
    123     ];
    124     if value["identity"]["discovery"]["enabled"] == true {
    125         expected_role_keys.push(
    126             value["identity"]["discovery"]["binding"]["expected_public_key"]
    127                 .as_str()
    128                 .expect("discovery key"),
    129         );
    130     }
    131     if expected_role_keys.iter().collect::<BTreeSet<_>>().len() != expected_role_keys.len() {
    132         return false;
    133     }
    134     if expected_role_keys
    135         .iter()
    136         .chain(trusted.iter())
    137         .chain(denied.iter())
    138         .any(|key| PublicKey::from_hex(key).map_or(true, |public_key| public_key.xonly().is_err()))
    139     {
    140         return false;
    141     }
    142 
    143     let permitted_kinds = string_array(value, "/policy/permission_ceiling")
    144         .into_iter()
    145         .filter_map(|permission| permission.strip_prefix("sign_event:kind:"))
    146         .filter_map(|kind| kind.parse::<u64>().ok())
    147         .collect::<BTreeSet<_>>();
    148     let allowed_kinds = value["policy"]["allowed_sign_event_kinds"]
    149         .as_array()
    150         .expect("allowed kinds")
    151         .iter()
    152         .map(|kind| kind.as_u64().expect("event kind"))
    153         .collect::<BTreeSet<_>>();
    154     if permitted_kinds != allowed_kinds {
    155         return false;
    156     }
    157 
    158     let challenges = &value["policy"]["challenges"];
    159     if challenges["enabled"] == true
    160         && challenges["authorized_lifetime_ms"].as_u64()
    161             < challenges["pending_lifetime_ms"].as_u64()
    162     {
    163         return false;
    164     }
    165     for (name, expected_scope) in [
    166         ("connection_admission", "global_and_relay"),
    167         ("challenge_creation", "connection"),
    168         ("challenge_authorization", "connection"),
    169     ] {
    170         let limit = &value["rate_limits"][name];
    171         if limit["scope"] != expected_scope
    172             || limit["retention_ms"].as_u64() < limit["window_ms"].as_u64()
    173         {
    174             return false;
    175         }
    176     }
    177 
    178     let retry = &value["transport"]["publish_retry"];
    179     if !retry.is_null()
    180         && retry["initial_backoff_ms"].as_u64() > retry["maximum_backoff_ms"].as_u64()
    181     {
    182         return false;
    183     }
    184     let ingress = &value["transport"]["ingress"];
    185     if !(1000..=300000).contains(&ingress["subscription_deadline_ms"].as_u64().unwrap_or(0))
    186         || ingress["maximum_past_seconds"].as_u64() > Some(3600)
    187         || ingress["maximum_future_seconds"].as_u64() > Some(3600)
    188     {
    189         return false;
    190     }
    191     let required_writers = relays
    192         .iter()
    193         .filter(|relay| {
    194             relay["required"].as_bool() == Some(true) && relay["write"].as_bool() == Some(true)
    195         })
    196         .count() as u64;
    197     let delivery = &value["transport"]["delivery_policy"];
    198     if delivery["mode"] == "required_quorum"
    199         && delivery["required_acknowledgements"].as_u64() > Some(required_writers)
    200     {
    201         return false;
    202     }
    203 
    204     let discovery_enabled = value["discovery"]["enabled"].as_bool();
    205     if discovery_enabled != value["identity"]["discovery"]["enabled"].as_bool() {
    206         return false;
    207     }
    208     if discovery_enabled == Some(true) {
    209         let by_id = relays
    210             .iter()
    211             .map(|relay| (relay["id"].as_str().unwrap(), relay))
    212             .collect::<BTreeMap<_, _>>();
    213         if string_array(value, "/discovery/public_relay_ids")
    214             .iter()
    215             .any(|id| by_id.get(id).and_then(|relay| relay["read"].as_bool()) != Some(true))
    216             || string_array(value, "/discovery/publish_relay_ids")
    217                 .iter()
    218                 .any(|id| by_id.get(id).and_then(|relay| relay["write"].as_bool()) != Some(true))
    219         {
    220             return false;
    221         }
    222     }
    223 
    224     let operations = &value["operations"];
    225     if operations["enabled"] == true {
    226         let Ok(listen) = operations["listen"]
    227             .as_str()
    228             .unwrap_or_default()
    229             .parse::<SocketAddr>()
    230         else {
    231             return false;
    232         };
    233         if listen.port() == 0
    234             || (operations["bind_policy"] == "loopback_only" && !listen.ip().is_loopback())
    235         {
    236             return false;
    237         }
    238     }
    239     true
    240 }
    241 
    242 fn assert_rejected(value: &Value, profile: Profile) {
    243     assert!(
    244         !semantic_valid(value, profile),
    245         "negative configuration vector unexpectedly passed"
    246     );
    247 }
    248 
    249 fn insert(value: &mut Value, pointer: &str, key: &str, replacement: Value) {
    250     value
    251         .pointer_mut(pointer)
    252         .and_then(Value::as_object_mut)
    253         .expect("object pointer")
    254         .insert(key.to_owned(), replacement);
    255 }
    256 
    257 fn defaults(value: &Value, pointer: &str, output: &mut BTreeMap<String, Value>) {
    258     match value {
    259         Value::Object(object) => {
    260             if let Some(default) = object.get("default") {
    261                 assert!(
    262                     object.contains_key("x-radroots-default-source"),
    263                     "default without provenance at {pointer}"
    264                 );
    265                 output.insert(pointer.to_owned(), default.clone());
    266             }
    267             for (key, child) in object {
    268                 defaults(child, &format!("{pointer}/{key}"), output);
    269             }
    270         }
    271         Value::Array(values) => {
    272             for (index, child) in values.iter().enumerate() {
    273                 defaults(child, &format!("{pointer}/{index}"), output);
    274             }
    275         }
    276         _ => {}
    277     }
    278 }
    279 
    280 fn assert_integer_bounds(value: &Value, pointer: &str) {
    281     match value {
    282         Value::Object(object) => {
    283             if object.get("type") == Some(&Value::String("integer".to_owned())) {
    284                 assert!(
    285                     object.contains_key("minimum"),
    286                     "missing minimum at {pointer}"
    287                 );
    288                 assert!(
    289                     object.contains_key("maximum"),
    290                     "missing maximum at {pointer}"
    291                 );
    292             }
    293             for (key, child) in object {
    294                 assert_integer_bounds(child, &format!("{pointer}/{key}"));
    295             }
    296         }
    297         Value::Array(values) => {
    298             for (index, child) in values.iter().enumerate() {
    299                 assert_integer_bounds(child, &format!("{pointer}/{index}"));
    300             }
    301         }
    302         _ => {}
    303     }
    304 }
    305 
    306 #[test]
    307 fn schema_identity_structure_and_machine_policy_are_exact() {
    308     let schema = schema();
    309     assert_eq!(
    310         schema["$schema"],
    311         "https://json-schema.org/draft/2020-12/schema"
    312     );
    313     assert_eq!(schema["type"], "object");
    314     assert_eq!(schema["additionalProperties"], false);
    315     assert_eq!(
    316         schema["required"],
    317         json!([
    318             "schema",
    319             "schema_version",
    320             "service",
    321             "logging",
    322             "operations",
    323             "database",
    324             "identity",
    325             "relays",
    326             "transport",
    327             "policy",
    328             "rate_limits",
    329             "resource_limits",
    330             "discovery"
    331         ])
    332     );
    333     assert_eq!(
    334         schema["properties"]["schema"]["const"],
    335         "radroots.myc.config"
    336     );
    337     assert_eq!(schema["properties"]["schema_version"]["const"], 1);
    338     assert_eq!(
    339         schema["x-radroots-contract"]["bootstrap_only"],
    340         json!(["profile", "instance", "repo_local_root", "config_path"])
    341     );
    342     assert_eq!(
    343         schema["x-radroots-contract"]["document_max_utf8_bytes"],
    344         1_048_576
    345     );
    346     assert_eq!(
    347         schema["x-radroots-contract"]["duplicate_keys"],
    348         "reject_on_original_wire"
    349     );
    350     assert_eq!(
    351         schema["x-radroots-contract"]["null_values"],
    352         "reject_on_original_wire"
    353     );
    354     assert_eq!(
    355         schema["x-radroots-contract"]["environment_overlay"],
    356         "forbidden"
    357     );
    358     assert_eq!(
    359         schema["x-radroots-contract"]["protected_material"],
    360         "forbidden"
    361     );
    362     assert_eq!(
    363         schema["x-radroots-contract"]["effective_output"],
    364         "deterministic_redacted_with_exact_provenance"
    365     );
    366     assert_integer_bounds(&schema, "");
    367 
    368     let mut found_defaults = BTreeMap::new();
    369     defaults(&schema, "", &mut found_defaults);
    370     assert_eq!(found_defaults.len(), 41);
    371     for source in found_defaults.keys().map(|pointer| {
    372         schema.pointer(pointer).unwrap()["x-radroots-default-source"]
    373             .as_str()
    374             .unwrap()
    375     }) {
    376         assert!(
    377             schema["x-radroots-contract"]["safe_default_sources"]
    378                 .as_array()
    379                 .unwrap()
    380                 .iter()
    381                 .any(|allowed| allowed == source)
    382         );
    383     }
    384 }
    385 
    386 #[test]
    387 fn lib_derived_limits_and_defaults_are_literal_frozen() {
    388     let schema = schema();
    389     assert_eq!(
    390         schema["$defs"]["runtime_limits"]["properties"]["worker_threads"],
    391         json!({
    392             "type": "integer",
    393             "minimum": 2,
    394             "maximum": 32,
    395             "default": 4,
    396             "x-radroots-default-source": "engineering_safety",
    397         })
    398     );
    399     assert_eq!(
    400         schema["$defs"]["runtime_limits"]["properties"]["blocking_threads"],
    401         json!({
    402             "type": "integer",
    403             "minimum": 1,
    404             "maximum": 32,
    405             "default": 8,
    406             "x-radroots-default-source": "engineering_safety",
    407         })
    408     );
    409     assert_eq!(
    410         schema["$defs"]["operations_limits"]["properties"]["header_bytes"]["minimum"],
    411         8_192
    412     );
    413     assert_eq!(
    414         schema["$defs"]["admin_limits"]["properties"]["response_body_utf8_bytes"]["minimum"],
    415         8_382
    416     );
    417     let exact = [
    418         ("/$defs/operations_limits/properties/header_count", 64, 32),
    419         (
    420             "/$defs/operations_limits/properties/header_bytes",
    421             32_768,
    422             16_384,
    423         ),
    424         (
    425             "/$defs/operations_limits/properties/response_body_utf8_bytes",
    426             1_048_576,
    427             1_048_576,
    428         ),
    429         (
    430             "/$defs/operations_limits/properties/concurrent_connections",
    431             64,
    432             32,
    433         ),
    434         (
    435             "/$defs/operations_limits/properties/request_deadline_ms",
    436             30_000,
    437             15_000,
    438         ),
    439         (
    440             "/$defs/operations_limits/properties/idle_timeout_ms",
    441             60_000,
    442             30_000,
    443         ),
    444         ("/$defs/database/properties/busy_timeout_ms", 60_000, 5_000),
    445         ("/$defs/database/properties/max_connections", 8, 8),
    446         ("/$defs/admin_limits/properties/header_count", 64, 32),
    447         (
    448             "/$defs/admin_limits/properties/header_bytes",
    449             32_768,
    450             16_384,
    451         ),
    452         (
    453             "/$defs/admin_limits/properties/request_body_utf8_bytes",
    454             65_536,
    455             65_536,
    456         ),
    457         (
    458             "/$defs/admin_limits/properties/response_body_utf8_bytes",
    459             1_048_576,
    460             1_048_576,
    461         ),
    462         (
    463             "/$defs/admin_limits/properties/concurrent_connections",
    464             64,
    465             32,
    466         ),
    467         (
    468             "/$defs/admin_limits/properties/request_deadline_ms",
    469             30_000,
    470             15_000,
    471         ),
    472         (
    473             "/$defs/admin_limits/properties/idle_timeout_ms",
    474             60_000,
    475             30_000,
    476         ),
    477         ("/$defs/admin_limits/properties/query_items", 200, 100),
    478         ("/$defs/metrics_limits/properties/descriptors", 64, 64),
    479         ("/$defs/metrics_limits/properties/samples", 512, 512),
    480         ("/$defs/metrics_limits/properties/labels_per_sample", 8, 8),
    481         (
    482             "/$defs/metrics_limits/properties/render_utf8_bytes",
    483             1_048_576,
    484             1_048_576,
    485         ),
    486         (
    487             "/$defs/event_limits/properties/wire_bytes",
    488             524_288,
    489             262_144,
    490         ),
    491         (
    492             "/$defs/event_limits/properties/content_bytes",
    493             131_072,
    494             131_072,
    495         ),
    496         ("/$defs/event_limits/properties/tag_count", 1_024, 1_024),
    497         (
    498             "/$defs/event_limits/properties/tag_total_elements",
    499             4_096,
    500             4_096,
    501         ),
    502         (
    503             "/$defs/event_limits/properties/tag_element_bytes",
    504             4_096,
    505             4_096,
    506         ),
    507         (
    508             "/$defs/event_limits/properties/tag_total_bytes",
    509             131_072,
    510             131_072,
    511         ),
    512         (
    513             "/$defs/event_limits/properties/decrypted_plaintext_bytes",
    514             262_144,
    515             262_144,
    516         ),
    517     ];
    518     for (pointer, maximum, default) in exact {
    519         assert_eq!(
    520             schema.pointer(pointer).unwrap()["maximum"],
    521             maximum,
    522             "{pointer}"
    523         );
    524         assert_eq!(
    525             schema.pointer(pointer).unwrap()["default"],
    526             default,
    527             "{pointer}"
    528         );
    529     }
    530 }
    531 
    532 #[test]
    533 fn canonical_example_and_required_positive_variants_pass() {
    534     let value = example();
    535     assert!(semantic_valid(&value, Profile::Production));
    536     assert_eq!(value["identity"]["transport"]["provider"], "encrypted_file");
    537     assert_eq!(value["identity"]["user"]["provider"], "local_signer");
    538     assert_eq!(
    539         value["identity"]["discovery"]["binding"]["provider"],
    540         "encrypted_file"
    541     );
    542 
    543     let mut disabled_discovery = value.clone();
    544     disabled_discovery["identity"]["discovery"] = json!({"enabled": false});
    545     disabled_discovery["discovery"] = json!({"enabled": false});
    546     assert!(semantic_valid(&disabled_discovery, Profile::Production));
    547 
    548     let mut repo_local = disabled_discovery.clone();
    549     repo_local["relays"][0]["url"] = json!("ws://127.0.0.1:8080/");
    550     repo_local["relays"][1]["url"] = json!("ws://localhost:8081/");
    551     assert!(semantic_valid(&repo_local, Profile::RepoLocal));
    552     assert_rejected(&repo_local, Profile::Production);
    553 
    554     let mut enabled_operations = value;
    555     enabled_operations["operations"] = json!({
    556         "enabled": true,
    557         "listen": "127.0.0.1:9460",
    558         "bind_policy": "loopback_only",
    559         "limits": {}
    560     });
    561     assert!(semantic_valid(&enabled_operations, Profile::Production));
    562 }
    563 
    564 #[test]
    565 fn structural_unknown_legacy_and_original_wire_vectors_fail() {
    566     let value = example();
    567     for (pointer, key) in [
    568         ("", "unknown"),
    569         ("/service", "shutdowm_grace_ms"),
    570         ("/identity/transport", "executable"),
    571         ("/resource_limits/admin", "body_bytes"),
    572         ("/policy/challenges", "redirect_from_client"),
    573     ] {
    574         let mut invalid = value.clone();
    575         insert(&mut invalid, pointer, key, json!(true));
    576         assert_rejected(&invalid, Profile::Production);
    577     }
    578     for field in schema()["x-radroots-contract"]["forbidden_prototype_fields"]
    579         .as_array()
    580         .unwrap()
    581     {
    582         let mut invalid = value.clone();
    583         invalid.as_object_mut().unwrap().insert(
    584             field.as_str().unwrap().to_owned(),
    585             Value::Object(Map::new()),
    586         );
    587         assert_rejected(&invalid, Profile::Production);
    588     }
    589 
    590     assert!(toml::from_str::<toml::Value>("schema='a'\nschema='b'\n").is_err());
    591     assert!(toml::from_str::<toml::Value>("schema = null\n").is_err());
    592 }
    593 
    594 #[test]
    595 fn identity_relay_and_network_authority_fail_closed() {
    596     let value = example();
    597     for pointer in [
    598         "/identity/transport/envelope_path",
    599         "/identity/user/socket_path",
    600     ] {
    601         let mut invalid = value.clone();
    602         *invalid.pointer_mut(pointer).unwrap() = json!("relative/path");
    603         assert_rejected(&invalid, Profile::Production);
    604     }
    605     for provider in [
    606         "plaintext_file",
    607         "external_command",
    608         "host_vault",
    609         "keyring",
    610         "managed_account",
    611     ] {
    612         let mut invalid = value.clone();
    613         invalid["identity"]["transport"]["provider"] = json!(provider);
    614         assert_rejected(&invalid, Profile::Production);
    615     }
    616 
    617     let mut duplicate_id = value.clone();
    618     duplicate_id["relays"][1]["id"] = duplicate_id["relays"][0]["id"].clone();
    619     assert_rejected(&duplicate_id, Profile::Production);
    620     let mut duplicate_url = value.clone();
    621     duplicate_url["relays"][1]["url"] = duplicate_url["relays"][0]["url"].clone();
    622     assert_rejected(&duplicate_url, Profile::Production);
    623     let mut inactive = value.clone();
    624     inactive["relays"][0]["read"] = json!(false);
    625     inactive["relays"][0]["write"] = json!(false);
    626     assert_rejected(&inactive, Profile::Production);
    627     let mut insecure = value;
    628     insecure["relays"][0]["url"] = json!("ws://127.0.0.1:8080/");
    629     assert_rejected(&insecure, Profile::Production);
    630 
    631     let mut collapsed_roles = example();
    632     collapsed_roles["identity"]["user"]["expected_public_key"] =
    633         collapsed_roles["identity"]["transport"]["expected_public_key"].clone();
    634     assert_rejected(&collapsed_roles, Profile::Production);
    635 
    636     let mut noncanonical_url = example();
    637     noncanonical_url["relays"][0]["url"] = json!("WSS://relay-primary.example.test");
    638     assert_rejected(&noncanonical_url, Profile::Production);
    639     let mut credential_name = example();
    640     credential_name["identity"]["transport"]["credential_reference"] =
    641         json!("transport.identity-key");
    642     assert!(semantic_valid(&credential_name, Profile::Production));
    643     credential_name["identity"]["transport"]["credential_reference"] =
    644         json!(format!("a{}z", "x".repeat(126)));
    645     assert!(semantic_valid(&credential_name, Profile::Production));
    646     credential_name["identity"]["transport"]["credential_reference"] =
    647         json!(format!("a{}z", "x".repeat(127)));
    648     assert_rejected(&credential_name, Profile::Production);
    649 }
    650 
    651 #[test]
    652 fn bounds_relationships_and_conditional_authority_fail_closed() {
    653     let value = example();
    654     for (pointer, exact, over) in [
    655         ("/database/busy_timeout_ms", 60_000_u64, 60_001_u64),
    656         ("/database/max_connections", 8, 9),
    657         ("/resource_limits/admin/header_count", 64, 65),
    658         ("/resource_limits/admin/query_items", 200, 201),
    659         ("/resource_limits/events/wire_bytes", 524_288, 524_289),
    660         ("/resource_limits/metrics/samples", 512, 513),
    661         ("/identity/user/request_deadline_ms", 30_000, 30_001),
    662         ("/identity/user/concurrency", 64, 65),
    663     ] {
    664         let mut minimum = value.clone();
    665         *minimum.pointer_mut(pointer).unwrap() = json!(1);
    666         assert!(
    667             semantic_valid(&minimum, Profile::Production),
    668             "minimum {pointer}"
    669         );
    670         let mut maximum = value.clone();
    671         *maximum.pointer_mut(pointer).unwrap() = json!(exact);
    672         assert!(
    673             semantic_valid(&maximum, Profile::Production),
    674             "maximum {pointer}"
    675         );
    676         let mut zero = value.clone();
    677         *zero.pointer_mut(pointer).unwrap() = json!(0);
    678         assert_rejected(&zero, Profile::Production);
    679         let mut excessive = value.clone();
    680         *excessive.pointer_mut(pointer).unwrap() = json!(over);
    681         assert_rejected(&excessive, Profile::Production);
    682     }
    683 
    684     let mut exact_admin_response = value.clone();
    685     exact_admin_response["resource_limits"]["admin"]["response_body_utf8_bytes"] = json!(8_382);
    686     assert!(semantic_valid(&exact_admin_response, Profile::Production));
    687     let mut undersized_admin_response = value.clone();
    688     undersized_admin_response["resource_limits"]["admin"]["response_body_utf8_bytes"] =
    689         json!(8_381);
    690     assert_rejected(&undersized_admin_response, Profile::Production);
    691 
    692     let mut overlap = value.clone();
    693     overlap["policy"]["denied_clients"] = overlap["policy"]["trusted_clients"].clone();
    694     assert_rejected(&overlap, Profile::Production);
    695     for pointer in [
    696         "/identity/transport/expected_public_key",
    697         "/identity/user/expected_public_key",
    698         "/identity/discovery/binding/expected_public_key",
    699         "/policy/trusted_clients/0",
    700         "/policy/denied_clients/0",
    701     ] {
    702         let mut invalid_key = value.clone();
    703         *invalid_key.pointer_mut(pointer).expect("public key") = json!("f".repeat(64));
    704         assert_rejected(&invalid_key, Profile::Production);
    705     }
    706     let mut permission_mismatch = value.clone();
    707     permission_mismatch["policy"]["allowed_sign_event_kinds"] = json!([1, 7]);
    708     assert_rejected(&permission_mismatch, Profile::Production);
    709     let mut challenge_lifetime = value.clone();
    710     challenge_lifetime["policy"]["challenges"]["authorized_lifetime_ms"] = json!(1000);
    711     assert_rejected(&challenge_lifetime, Profile::Production);
    712     let mut challenge_missing = value.clone();
    713     challenge_missing["policy"]["challenges"]
    714         .as_object_mut()
    715         .unwrap()
    716         .remove("url");
    717     assert_rejected(&challenge_missing, Profile::Production);
    718     let mut challenge_disabled_leak = value.clone();
    719     challenge_disabled_leak["policy"]["challenges"]["enabled"] = json!(false);
    720     assert_rejected(&challenge_disabled_leak, Profile::Production);
    721     let mut rate_retention = value.clone();
    722     rate_retention["rate_limits"]["challenge_creation"]["retention_ms"] = json!(1);
    723     assert_rejected(&rate_retention, Profile::Production);
    724     let mut backoff = value.clone();
    725     backoff["transport"]["publish_retry"]["initial_backoff_ms"] = json!(30_000);
    726     backoff["transport"]["publish_retry"]["maximum_backoff_ms"] = json!(1);
    727     assert_rejected(&backoff, Profile::Production);
    728     for (field, invalid_values) in [
    729         ("subscription_deadline_ms", vec![json!(999), json!(300001)]),
    730         ("maximum_past_seconds", vec![json!(3601), json!(-1)]),
    731         ("maximum_future_seconds", vec![json!(3601), json!(-1)]),
    732     ] {
    733         for invalid_value in invalid_values {
    734             let mut invalid = value.clone();
    735             invalid["transport"]["ingress"][field] = invalid_value;
    736             assert_rejected(&invalid, Profile::Production);
    737         }
    738         let mut missing = value.clone();
    739         missing["transport"]["ingress"]
    740             .as_object_mut()
    741             .expect("ingress object")
    742             .remove(field);
    743         assert_rejected(&missing, Profile::Production);
    744     }
    745     let mut quorum = value.clone();
    746     quorum["transport"]["delivery_policy"] =
    747         json!({"mode": "required_quorum", "required_acknowledgements": 3});
    748     assert_rejected(&quorum, Profile::Production);
    749     let mut discovery_mismatch = value.clone();
    750     discovery_mismatch["identity"]["discovery"] = json!({"enabled": false});
    751     assert_rejected(&discovery_mismatch, Profile::Production);
    752     let mut missing_relay = value.clone();
    753     missing_relay["discovery"]["publish_relay_ids"] = json!(["missing"]);
    754     assert_rejected(&missing_relay, Profile::Production);
    755     let mut invalid_template = value;
    756     invalid_template["discovery"]["nostrconnect_url_template"] =
    757         json!("https://myc.example.test/connect");
    758     assert_rejected(&invalid_template, Profile::Production);
    759 }
    760 
    761 #[test]
    762 fn schema_and_version_are_closed_and_defaults_are_only_safe_leaves() {
    763     let value = example();
    764     for replacement in [json!("radroots.myc.config.v2"), json!("myc")] {
    765         let mut invalid = value.clone();
    766         invalid["schema"] = replacement;
    767         assert_rejected(&invalid, Profile::Production);
    768     }
    769     for replacement in [json!(0), json!(2), json!("1")] {
    770         let mut invalid = value.clone();
    771         invalid["schema_version"] = replacement;
    772         assert_rejected(&invalid, Profile::Production);
    773     }
    774 
    775     let mut safe_defaults_omitted = value;
    776     safe_defaults_omitted["service"] = json!({});
    777     safe_defaults_omitted["logging"] = json!({});
    778     safe_defaults_omitted["database"]
    779         .as_object_mut()
    780         .unwrap()
    781         .remove("busy_timeout_ms");
    782     safe_defaults_omitted["database"]
    783         .as_object_mut()
    784         .unwrap()
    785         .remove("max_connections");
    786     safe_defaults_omitted["transport"]
    787         .as_object_mut()
    788         .unwrap()
    789         .remove("connect_deadline_ms");
    790     safe_defaults_omitted["transport"]
    791         .as_object_mut()
    792         .unwrap()
    793         .remove("publish_retry");
    794     safe_defaults_omitted["resource_limits"] = json!({});
    795     assert!(semantic_valid(&safe_defaults_omitted, Profile::Production));
    796 
    797     for required in [
    798         "minimum_free_bytes",
    799         "transport",
    800         "user",
    801         "relays",
    802         "connection_approval",
    803         "trusted_clients",
    804         "denied_clients",
    805         "permission_ceiling",
    806         "allowed_sign_event_kinds",
    807         "challenges",
    808         "rate_limits",
    809         "delivery_policy",
    810         "subscription_deadline_ms",
    811         "maximum_past_seconds",
    812         "maximum_future_seconds",
    813         "discovery",
    814     ] {
    815         assert!(
    816             CONFIG_SCHEMA.contains(required),
    817             "missing authority {required}"
    818         );
    819     }
    820 }
    821 
    822 #[test]
    823 fn canonical_example_contains_no_protected_material_or_legacy_selector() {
    824     let lowercase = CONFIG_EXAMPLE.to_ascii_lowercase();
    825     for forbidden in [
    826         "private_key",
    827         "secret_key",
    828         "mnemonic",
    829         "nsec1",
    830         "wrapping_key =",
    831         "myc_",
    832         "env_file",
    833         "external_command",
    834         "plaintext_file",
    835         "keyring",
    836         "managed_account",
    837     ] {
    838         assert!(
    839             !lowercase.contains(forbidden),
    840             "canonical example contains forbidden material or selector {forbidden}"
    841         );
    842     }
    843 }