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 }