services_hardening_config_contract.rs (29944B)
1 #![forbid(unsafe_code)] 2 3 use std::collections::{BTreeMap, BTreeSet}; 4 use std::net::SocketAddr; 5 use std::path::{Component, Path}; 6 7 use nostr::PublicKey; 8 use serde_json::{Map, Value, json}; 9 10 const CONFIG_SCHEMA: &str = include_str!("../contracts/services_hardening/config.v1.schema.json"); 11 const CONFIG_EXAMPLE: &str = include_str!("../contracts/services_hardening/config.v1.example.toml"); 12 const EVIDENCE_POLICY: &str = 13 include_str!("../contracts/services_hardening/evidence_policy.v1.json"); 14 15 #[derive(Clone, Copy)] 16 enum Profile { 17 Production, 18 RepoLocal, 19 } 20 21 fn schema() -> Value { 22 serde_json::from_str(CONFIG_SCHEMA).expect("configuration schema must be valid JSON") 23 } 24 25 fn evidence_policy() -> Value { 26 serde_json::from_str(EVIDENCE_POLICY).expect("evidence policy must be valid JSON") 27 } 28 29 fn example() -> Value { 30 let value = toml::from_str::<toml::Value>(CONFIG_EXAMPLE) 31 .expect("configuration example must be valid TOML"); 32 serde_json::to_value(value).expect("TOML value must convert to JSON") 33 } 34 35 fn schema_valid(value: &Value) -> bool { 36 jsonschema::validator_for(&schema()) 37 .expect("configuration schema must compile") 38 .is_valid(value) 39 } 40 41 fn string_array<'a>(value: &'a Value, pointer: &str) -> Vec<&'a str> { 42 value 43 .pointer(pointer) 44 .and_then(Value::as_array) 45 .expect("string array") 46 .iter() 47 .map(|value| value.as_str().expect("string item")) 48 .collect() 49 } 50 51 fn semantic_valid(value: &Value, profile: Profile) -> bool { 52 if !schema_valid(value) { 53 return false; 54 } 55 56 let relays = value["relays"].as_array().expect("relays"); 57 let relay_ids = relays 58 .iter() 59 .map(|relay| relay["id"].as_str().expect("relay id")) 60 .collect::<Vec<_>>(); 61 let relay_urls = relays 62 .iter() 63 .map(|relay| relay["url"].as_str().expect("relay URL")) 64 .collect::<Vec<_>>(); 65 let parsed_relay_urls = relay_urls 66 .iter() 67 .map(|raw| { 68 url::Url::parse(raw) 69 .ok() 70 .filter(|parsed| parsed.as_str() == *raw) 71 }) 72 .collect::<Vec<_>>(); 73 if relay_ids.iter().collect::<BTreeSet<_>>().len() != relay_ids.len() 74 || relay_urls.iter().collect::<BTreeSet<_>>().len() != relay_urls.len() 75 || parsed_relay_urls.iter().any(Option::is_none) 76 || parsed_relay_urls.iter().flatten().any(|url| { 77 !url.username().is_empty() || url.password().is_some() || url.fragment().is_some() 78 }) 79 || relays.iter().any(|relay| { 80 !relay["read"].as_bool().expect("read") && !relay["write"].as_bool().expect("write") 81 }) 82 { 83 return false; 84 } 85 if parsed_relay_urls.iter().flatten().any(|url| match profile { 86 Profile::Production => url.scheme() != "wss", 87 Profile::RepoLocal => match url.scheme() { 88 "wss" => false, 89 "ws" => !matches!( 90 url.host_str(), 91 Some("127.0.0.1" | "localhost" | "[::1]" | "::1") 92 ), 93 _ => true, 94 }, 95 }) { 96 return false; 97 } 98 let relays_by_id = relays 99 .iter() 100 .map(|relay| (relay["id"].as_str().expect("relay id"), relay)) 101 .collect::<BTreeMap<_, _>>(); 102 103 let envelope_path = value["identity"]["service"]["envelope_path"] 104 .as_str() 105 .expect("envelope path"); 106 let path = Path::new(envelope_path); 107 if envelope_path.len() > 4096 108 || path == Path::new("/") 109 || !path.is_absolute() 110 || path 111 .components() 112 .any(|component| matches!(component, Component::CurDir | Component::ParentDir)) 113 { 114 return false; 115 } 116 117 let Ok(public_key) = PublicKey::from_hex( 118 value["identity"]["service"]["expected_public_key"] 119 .as_str() 120 .expect("public key"), 121 ) else { 122 return false; 123 }; 124 if public_key.xonly().is_err() { 125 return false; 126 } 127 128 let sources = value["evidence"]["sources"] 129 .as_array() 130 .expect("evidence sources"); 131 let source_ids = sources 132 .iter() 133 .map(|source| source["source_id"].as_str().expect("source id")) 134 .collect::<Vec<_>>(); 135 let source_bindings = sources 136 .iter() 137 .map(|source| { 138 ( 139 source["kind"].as_str().expect("kind"), 140 source["relay_id"].as_str().expect("relay id"), 141 source["selector"].as_str().expect("selector"), 142 ) 143 }) 144 .collect::<Vec<_>>(); 145 if source_ids.iter().collect::<BTreeSet<_>>().len() != source_ids.len() 146 || source_bindings.iter().collect::<BTreeSet<_>>().len() != source_bindings.len() 147 || !sources 148 .iter() 149 .any(|source| source["required"].as_bool() == Some(true)) 150 || sources.iter().any(|source| { 151 source["overlap_seconds"].as_u64() > source["lookback_seconds"].as_u64() 152 || relays_by_id 153 .get(source["relay_id"].as_str().expect("relay id")) 154 .and_then(|relay| relay["read"].as_bool()) 155 != Some(true) 156 }) 157 { 158 return false; 159 } 160 161 let reconciliation = &value["reconciliation"]; 162 let lease_ms = reconciliation["lease_ms"].as_u64().unwrap_or(30_000); 163 let lease_renewal_ms = reconciliation["lease_renewal_ms"] 164 .as_u64() 165 .unwrap_or(10_000); 166 let initial_backoff_ms = reconciliation["initial_backoff_ms"].as_u64().unwrap_or(250); 167 let maximum_backoff_ms = reconciliation["maximum_backoff_ms"] 168 .as_u64() 169 .unwrap_or(30_000); 170 let attempt_deadline_ms = reconciliation["attempt_deadline_ms"] 171 .as_u64() 172 .unwrap_or(30_000); 173 if lease_renewal_ms >= lease_ms 174 || initial_backoff_ms > maximum_backoff_ms 175 || sources 176 .iter() 177 .any(|source| source["deadline_ms"].as_u64() > Some(attempt_deadline_ms)) 178 { 179 return false; 180 } 181 182 let publication = &value["publication"]; 183 if publication["mode"] == "required" 184 && (string_array(value, "/publication/target_relay_ids") 185 .iter() 186 .any(|id| { 187 relays_by_id 188 .get(id) 189 .and_then(|relay| relay["write"].as_bool()) 190 != Some(true) 191 }) 192 || publication["retry"]["initial_backoff_ms"] 193 .as_u64() 194 .unwrap_or(250) 195 > publication["retry"]["maximum_backoff_ms"] 196 .as_u64() 197 .unwrap_or(30_000)) 198 { 199 return false; 200 } 201 202 let presence = &value["presence"]; 203 if presence["enabled"] == true { 204 if !presence["profile"].as_bool().unwrap_or(false) 205 && !presence["application_handler"].as_bool().unwrap_or(false) 206 { 207 return false; 208 } 209 if string_array(value, "/presence/target_relay_ids") 210 .iter() 211 .any(|id| { 212 relays_by_id 213 .get(id) 214 .and_then(|relay| relay["write"].as_bool()) 215 != Some(true) 216 }) 217 { 218 return false; 219 } 220 } else if presence["profile"] != false || presence["application_handler"] != false { 221 return false; 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 if value["resource_limits"]["source_results"]["events"] 240 .as_u64() 241 .unwrap_or(4_096) 242 != 4_096 243 || value["resource_limits"]["source_results"]["bytes"] 244 .as_u64() 245 .unwrap_or(8_388_608) 246 != 8_388_608 247 { 248 return false; 249 } 250 true 251 } 252 253 fn assert_rejected(value: &Value, profile: Profile) { 254 assert!( 255 !semantic_valid(value, profile), 256 "negative configuration vector unexpectedly passed" 257 ); 258 } 259 260 fn insert(value: &mut Value, pointer: &str, key: &str, replacement: Value) { 261 value 262 .pointer_mut(pointer) 263 .and_then(Value::as_object_mut) 264 .expect("object pointer") 265 .insert(key.to_owned(), replacement); 266 } 267 268 fn defaults(value: &Value, pointer: &str, output: &mut BTreeMap<String, Value>) { 269 match value { 270 Value::Object(object) => { 271 if let Some(default) = object.get("default") { 272 assert!( 273 object.contains_key("x-radroots-default-source"), 274 "default without provenance at {pointer}" 275 ); 276 output.insert(pointer.to_owned(), default.clone()); 277 } 278 for (key, child) in object { 279 defaults(child, &format!("{pointer}/{key}"), output); 280 } 281 } 282 Value::Array(values) => { 283 for (index, child) in values.iter().enumerate() { 284 defaults(child, &format!("{pointer}/{index}"), output); 285 } 286 } 287 _ => {} 288 } 289 } 290 291 fn assert_integer_bounds(value: &Value, pointer: &str) { 292 match value { 293 Value::Object(object) => { 294 if object.get("type") == Some(&Value::String("integer".to_owned())) { 295 assert!( 296 object.contains_key("minimum"), 297 "missing minimum at {pointer}" 298 ); 299 assert!( 300 object.contains_key("maximum"), 301 "missing maximum at {pointer}" 302 ); 303 } 304 for (key, child) in object { 305 assert_integer_bounds(child, &format!("{pointer}/{key}")); 306 } 307 } 308 Value::Array(values) => { 309 for (index, child) in values.iter().enumerate() { 310 assert_integer_bounds(child, &format!("{pointer}/{index}")); 311 } 312 } 313 _ => {} 314 } 315 } 316 317 fn assert_object_schemas_are_closed(value: &Value, pointer: &str) { 318 match value { 319 Value::Object(object) => { 320 if object.get("type") == Some(&Value::String("object".to_owned())) { 321 assert_eq!( 322 object.get("additionalProperties"), 323 Some(&Value::Bool(false)), 324 "open object schema at {pointer}" 325 ); 326 } 327 for (key, child) in object { 328 assert_object_schemas_are_closed(child, &format!("{pointer}/{key}")); 329 } 330 } 331 Value::Array(values) => { 332 for (index, child) in values.iter().enumerate() { 333 assert_object_schemas_are_closed(child, &format!("{pointer}/{index}")); 334 } 335 } 336 _ => {} 337 } 338 } 339 340 #[test] 341 fn schema_identity_structure_and_machine_policy_are_exact() { 342 let schema = schema(); 343 assert_eq!( 344 schema["$schema"], 345 "https://json-schema.org/draft/2020-12/schema" 346 ); 347 assert_eq!(schema["type"], "object"); 348 assert_eq!(schema["additionalProperties"], false); 349 assert_eq!( 350 schema["required"], 351 json!([ 352 "schema", 353 "schema_version", 354 "service", 355 "logging", 356 "operations", 357 "database", 358 "identity", 359 "relays", 360 "network", 361 "evidence", 362 "reconciliation", 363 "attestation", 364 "publication", 365 "presence", 366 "resource_limits", 367 "retention" 368 ]) 369 ); 370 assert_eq!( 371 schema["properties"]["schema"]["const"], 372 "radroots.rhi.config" 373 ); 374 assert_eq!(schema["properties"]["schema_version"]["const"], 1); 375 assert_eq!( 376 schema["x-radroots-contract"]["bootstrap_only"], 377 json!(["profile", "instance", "repo_local_root", "config_path"]) 378 ); 379 assert_eq!( 380 schema["x-radroots-contract"]["document_max_utf8_bytes"], 381 1_048_576 382 ); 383 assert_eq!( 384 schema["x-radroots-contract"]["duplicate_keys"], 385 "reject_on_original_wire" 386 ); 387 assert_eq!( 388 schema["x-radroots-contract"]["null_values"], 389 "reject_on_original_wire" 390 ); 391 assert_eq!( 392 schema["x-radroots-contract"]["environment_overlay"], 393 "forbidden" 394 ); 395 assert_eq!( 396 schema["x-radroots-contract"]["protected_material"], 397 "forbidden" 398 ); 399 assert_eq!( 400 schema["x-radroots-contract"]["effective_output"], 401 "deterministic_redacted_with_exact_provenance" 402 ); 403 assert_eq!( 404 schema["x-radroots-contract"]["evidence_policy_sha256"], 405 "b4da4aa2863905975c577668bc75cbb03857106a67899278130915e085319dc8" 406 ); 407 assert_integer_bounds(&schema, ""); 408 assert_object_schemas_are_closed(&schema, ""); 409 410 let mut found_defaults = BTreeMap::new(); 411 defaults(&schema, "", &mut found_defaults); 412 assert!(!found_defaults.is_empty()); 413 for pointer in found_defaults.keys() { 414 let source = schema.pointer(pointer).expect("default pointer")["x-radroots-default-source"] 415 .as_str() 416 .expect("default source"); 417 assert!( 418 schema["x-radroots-contract"]["safe_default_sources"] 419 .as_array() 420 .expect("safe default sources") 421 .iter() 422 .any(|allowed| allowed == source) 423 ); 424 } 425 } 426 427 #[test] 428 fn evidence_policy_is_consumed_without_reinterpretation() { 429 let schema = schema(); 430 let policy = evidence_policy(); 431 assert_eq!( 432 policy["configuration"]["contract"], 433 schema["$defs"]["evidence"]["properties"]["contract"]["const"] 434 ); 435 assert_eq!( 436 policy["configuration"]["contract_version"], 437 schema["$defs"]["evidence"]["properties"]["contract_version"]["const"] 438 ); 439 assert_eq!( 440 policy["configuration"]["sources"]["minimum_items"], 441 schema["$defs"]["evidence"]["properties"]["sources"]["minItems"] 442 ); 443 assert_eq!( 444 policy["configuration"]["sources"]["maximum_items"], 445 schema["$defs"]["evidence"]["properties"]["sources"]["maxItems"] 446 ); 447 for field in ["deadline_ms", "lookback_seconds", "overlap_seconds"] { 448 assert_eq!( 449 policy["configuration"]["source"][field]["minimum"], 450 schema["$defs"]["evidence_source"]["properties"][field]["minimum"] 451 ); 452 assert_eq!( 453 policy["configuration"]["source"][field]["maximum"], 454 schema["$defs"]["evidence_source"]["properties"][field]["maximum"] 455 ); 456 } 457 assert_eq!( 458 policy["source_kinds"]["qualified_v1"], 459 json!(["nostr_relay"]) 460 ); 461 assert_eq!(policy["selector"]["id"], "trade_mutation_lineage_v1"); 462 assert_eq!( 463 policy["completion"]["success_evidence"], 464 "nostr_eose_received_before_source_deadline" 465 ); 466 assert_eq!( 467 policy["selector"]["maximum_events_per_result"], 468 schema["$defs"]["source_result_limits"]["properties"]["events"]["maximum"] 469 ); 470 assert_eq!( 471 policy["selector"]["maximum_result_bytes"], 472 schema["$defs"]["source_result_limits"]["properties"]["bytes"]["maximum"] 473 ); 474 } 475 476 #[test] 477 fn canonical_example_and_required_positive_variants_pass() { 478 let value = example(); 479 let validator = jsonschema::validator_for(&schema()).expect("schema"); 480 let schema_errors = validator 481 .iter_errors(&value) 482 .map(|error| error.to_string()) 483 .collect::<Vec<_>>(); 484 assert!(schema_errors.is_empty(), "{schema_errors:?}"); 485 assert!(semantic_valid(&value, Profile::Production)); 486 assert_eq!(value["identity"]["service"]["provider"], "encrypted_file"); 487 assert_eq!( 488 value["attestation"], 489 json!({ 490 "contract": "radroots.rhi.evidence_attestation.v1", 491 "contract_version": 1, 492 "method": "signed_evidence_snapshot", 493 "reducer_contract": "radroots.trade.reducer.v1", 494 "reducer_contract_version": 1 495 }) 496 ); 497 assert_eq!( 498 value["evidence"], 499 evidence_policy()["configuration_fixture"] 500 ); 501 502 let mut publication_disabled = value.clone(); 503 publication_disabled["publication"] = json!({"mode": "disabled"}); 504 assert!(semantic_valid(&publication_disabled, Profile::Production)); 505 506 let mut presence_disabled = value.clone(); 507 presence_disabled["presence"] = json!({ 508 "enabled": false, 509 "profile": false, 510 "application_handler": false 511 }); 512 assert!(semantic_valid(&presence_disabled, Profile::Production)); 513 514 let mut repo_local = value.clone(); 515 repo_local["relays"][0]["url"] = json!("ws://127.0.0.1:8080/"); 516 repo_local["relays"][1]["url"] = json!("ws://localhost:8081/"); 517 assert!(semantic_valid(&repo_local, Profile::RepoLocal)); 518 assert_rejected(&repo_local, Profile::Production); 519 520 let mut enabled_operations = value; 521 enabled_operations["operations"] = json!({ 522 "enabled": true, 523 "listen": "127.0.0.1:9460", 524 "bind_policy": "loopback_only", 525 "limits": {} 526 }); 527 assert!(semantic_valid(&enabled_operations, Profile::Production)); 528 529 let mut sparse_retry = example(); 530 sparse_retry["publication"]["retry"] = json!({"initial_backoff_ms": 250}); 531 assert!(semantic_valid(&sparse_retry, Profile::Production)); 532 } 533 534 #[test] 535 fn structural_unknown_legacy_and_original_wire_vectors_fail() { 536 let value = example(); 537 for (pointer, key) in [ 538 ("", "unknown"), 539 ("/service", "shutdowm_grace_ms"), 540 ("/identity/service", "wrapping_key_path"), 541 ("/evidence/sources/0", "completion"), 542 ("/resource_limits/admin", "body_bytes"), 543 ] { 544 let mut invalid = value.clone(); 545 insert(&mut invalid, pointer, key, json!(true)); 546 assert_rejected(&invalid, Profile::Production); 547 } 548 for field in schema()["x-radroots-contract"]["forbidden_prototype_fields"] 549 .as_array() 550 .expect("forbidden fields") 551 { 552 let mut invalid = value.clone(); 553 invalid.as_object_mut().expect("config object").insert( 554 field.as_str().expect("field").to_owned(), 555 Value::Object(Map::new()), 556 ); 557 assert_rejected(&invalid, Profile::Production); 558 } 559 assert!(toml::from_str::<toml::Value>("schema='a'\nschema='b'\n").is_err()); 560 assert!( 561 toml::from_str::<toml::Value>( 562 "[service]\nshutdown_grace_ms=30000\nshutdown_grace_ms=30001\n" 563 ) 564 .is_err() 565 ); 566 assert!(toml::from_str::<toml::Value>("schema = null\n").is_err()); 567 } 568 569 #[test] 570 fn identity_relay_and_network_authority_fail_closed() { 571 let value = example(); 572 let mut relative = value.clone(); 573 relative["identity"]["service"]["envelope_path"] = json!("relative/path"); 574 assert_rejected(&relative, Profile::Production); 575 for path in ["/", "/var/lib/radroots/../escape.ncrypt"] { 576 let mut invalid = value.clone(); 577 invalid["identity"]["service"]["envelope_path"] = json!(path); 578 assert_rejected(&invalid, Profile::Production); 579 } 580 for provider in [ 581 "plaintext_file", 582 "external_command", 583 "keyring", 584 "managed_account", 585 ] { 586 let mut invalid = value.clone(); 587 invalid["identity"]["service"]["provider"] = json!(provider); 588 assert_rejected(&invalid, Profile::Production); 589 } 590 let mut invalid = value.clone(); 591 invalid["identity"]["service"]["expected_public_key"] = json!("f".repeat(64)); 592 assert_rejected(&invalid, Profile::Production); 593 let mut duplicate_id = value.clone(); 594 duplicate_id["relays"][1]["id"] = duplicate_id["relays"][0]["id"].clone(); 595 assert_rejected(&duplicate_id, Profile::Production); 596 let mut duplicate_url = value.clone(); 597 duplicate_url["relays"][1]["url"] = duplicate_url["relays"][0]["url"].clone(); 598 assert_rejected(&duplicate_url, Profile::Production); 599 let mut inactive = value.clone(); 600 inactive["relays"][0]["read"] = json!(false); 601 inactive["relays"][0]["write"] = json!(false); 602 assert_rejected(&inactive, Profile::Production); 603 let mut insecure = value.clone(); 604 insecure["relays"][0]["url"] = json!("ws://127.0.0.1:8080/"); 605 assert_rejected(&insecure, Profile::Production); 606 let mut noncanonical = value; 607 noncanonical["relays"][0]["url"] = json!("WSS://relay.example.com"); 608 assert_rejected(&noncanonical, Profile::Production); 609 } 610 611 #[test] 612 fn evidence_scope_bounds_and_relationships_fail_closed() { 613 let value = example(); 614 for (pointer, minimum, below, maximum, above) in [ 615 ("/evidence/sources/0/deadline_ms", 100, 99, 30_000, 30_001), 616 ( 617 "/evidence/sources/0/lookback_seconds", 618 60, 619 59, 620 2_678_400, 621 2_678_401, 622 ), 623 ("/evidence/sources/0/overlap_seconds", 1, 0, 86_400, 86_401), 624 ] { 625 for valid in [minimum, maximum] { 626 let mut candidate = value.clone(); 627 *candidate.pointer_mut(pointer).expect("bounded field") = json!(valid); 628 if pointer.ends_with("lookback_seconds") && valid == 60 { 629 candidate["evidence"]["sources"][0]["overlap_seconds"] = json!(60); 630 } 631 assert!(semantic_valid(&candidate, Profile::Production), "{pointer}"); 632 } 633 for invalid in [below, above] { 634 let mut candidate = value.clone(); 635 *candidate.pointer_mut(pointer).expect("bounded field") = json!(invalid); 636 assert_rejected(&candidate, Profile::Production); 637 } 638 } 639 let mut no_required = value.clone(); 640 no_required["evidence"]["sources"][0]["required"] = json!(false); 641 assert_rejected(&no_required, Profile::Production); 642 let mut overlap = value.clone(); 643 overlap["evidence"]["sources"][0]["lookback_seconds"] = json!(60); 644 overlap["evidence"]["sources"][0]["overlap_seconds"] = json!(61); 645 assert_rejected(&overlap, Profile::Production); 646 let mut missing_relay = value.clone(); 647 missing_relay["evidence"]["sources"][0]["relay_id"] = json!("missing"); 648 assert_rejected(&missing_relay, Profile::Production); 649 let mut write_only = value.clone(); 650 write_only["relays"][0]["read"] = json!(false); 651 assert_rejected(&write_only, Profile::Production); 652 for replacement in [json!("filesystem"), json!("publication_target")] { 653 let mut invalid = value.clone(); 654 invalid["evidence"]["sources"][0]["kind"] = replacement; 655 assert_rejected(&invalid, Profile::Production); 656 } 657 let mut selector = value.clone(); 658 selector["evidence"]["sources"][0]["selector"] = json!("all_events_v1"); 659 assert_rejected(&selector, Profile::Production); 660 let mut seventeen = value; 661 let source = seventeen["evidence"]["sources"][0].clone(); 662 seventeen["evidence"]["sources"] = Value::Array(vec![source; 17]); 663 assert_rejected(&seventeen, Profile::Production); 664 } 665 666 #[test] 667 fn reconciliation_publication_presence_and_operations_fail_closed() { 668 let value = example(); 669 let mut renewal = value.clone(); 670 renewal["reconciliation"]["lease_renewal_ms"] = renewal["reconciliation"]["lease_ms"].clone(); 671 assert_rejected(&renewal, Profile::Production); 672 let mut backoff = value.clone(); 673 backoff["reconciliation"]["initial_backoff_ms"] = json!(30_001); 674 assert_rejected(&backoff, Profile::Production); 675 let mut attempt = value.clone(); 676 attempt["reconciliation"]["attempt_deadline_ms"] = json!(9_999); 677 assert_rejected(&attempt, Profile::Production); 678 let mut publication_relay = value.clone(); 679 publication_relay["publication"]["target_relay_ids"] = json!(["missing"]); 680 assert_rejected(&publication_relay, Profile::Production); 681 let mut disabled_leak = value.clone(); 682 disabled_leak["publication"]["mode"] = json!("disabled"); 683 assert_rejected(&disabled_leak, Profile::Production); 684 let mut publication_backoff = value.clone(); 685 publication_backoff["publication"]["retry"]["initial_backoff_ms"] = json!(30_001); 686 assert_rejected(&publication_backoff, Profile::Production); 687 let mut presence_relay = value.clone(); 688 presence_relay["presence"]["target_relay_ids"] = json!(["missing"]); 689 assert_rejected(&presence_relay, Profile::Production); 690 let mut no_desired_presence = value.clone(); 691 no_desired_presence["presence"]["profile"] = json!(false); 692 no_desired_presence["presence"]["application_handler"] = json!(false); 693 assert_rejected(&no_desired_presence, Profile::Production); 694 let mut disabled_presence_leak = value.clone(); 695 disabled_presence_leak["presence"]["enabled"] = json!(false); 696 assert_rejected(&disabled_presence_leak, Profile::Production); 697 let mut public_loopback_policy = value; 698 public_loopback_policy["operations"] = json!({ 699 "enabled": true, 700 "listen": "0.0.0.0:9460", 701 "bind_policy": "loopback_only", 702 "limits": {} 703 }); 704 assert_rejected(&public_loopback_policy, Profile::Production); 705 } 706 707 #[test] 708 fn exact_resource_boundaries_and_safe_defaults_are_frozen() { 709 let value = example(); 710 for (pointer, maximum, over) in [ 711 ("/database/busy_timeout_ms", 60_000_u64, 60_001_u64), 712 ("/database/max_connections", 8, 9), 713 ("/resource_limits/admin/header_count", 64, 65), 714 ("/resource_limits/admin/query_items", 200, 201), 715 ("/resource_limits/events/wire_bytes", 524_288, 524_289), 716 ("/resource_limits/metrics/samples", 512, 513), 717 ("/resource_limits/runtime/worker_threads", 32, 33), 718 ] { 719 let mut minimum = value.clone(); 720 *minimum.pointer_mut(pointer).expect("bounded field") = json!(1); 721 if pointer.ends_with("worker_threads") { 722 *minimum.pointer_mut(pointer).expect("bounded field") = json!(2); 723 } 724 assert!( 725 semantic_valid(&minimum, Profile::Production), 726 "minimum {pointer}" 727 ); 728 let mut exact = value.clone(); 729 *exact.pointer_mut(pointer).expect("bounded field") = json!(maximum); 730 assert!( 731 semantic_valid(&exact, Profile::Production), 732 "maximum {pointer}" 733 ); 734 let mut zero = value.clone(); 735 *zero.pointer_mut(pointer).expect("bounded field") = json!(0); 736 assert_rejected(&zero, Profile::Production); 737 let mut excessive = value.clone(); 738 *excessive.pointer_mut(pointer).expect("bounded field") = json!(over); 739 assert_rejected(&excessive, Profile::Production); 740 } 741 for (pointer, exact, lower, over) in [ 742 ( 743 "/resource_limits/source_results/events", 744 4_096, 745 4_095, 746 4_097, 747 ), 748 ( 749 "/resource_limits/source_results/bytes", 750 8_388_608, 751 8_388_607, 752 8_388_609, 753 ), 754 ] { 755 let mut exact_candidate = value.clone(); 756 *exact_candidate.pointer_mut(pointer).expect("bounded field") = json!(exact); 757 assert!(semantic_valid(&exact_candidate, Profile::Production)); 758 for invalid in [0, lower, over] { 759 let mut candidate = value.clone(); 760 *candidate.pointer_mut(pointer).expect("bounded field") = json!(invalid); 761 assert_rejected(&candidate, Profile::Production); 762 } 763 } 764 let mut exact_admin_response = value.clone(); 765 exact_admin_response["resource_limits"]["admin"]["response_body_utf8_bytes"] = json!(512); 766 assert!(semantic_valid(&exact_admin_response, Profile::Production)); 767 let mut undersized = value.clone(); 768 undersized["resource_limits"]["admin"]["response_body_utf8_bytes"] = json!(511); 769 assert_rejected(&undersized, Profile::Production); 770 771 let mut omitted = value; 772 omitted["service"] = json!({}); 773 omitted["logging"] = json!({}); 774 omitted["network"] = json!({}); 775 omitted["reconciliation"] = json!({}); 776 omitted["resource_limits"] = json!({}); 777 omitted["database"] 778 .as_object_mut() 779 .expect("database") 780 .remove("busy_timeout_ms"); 781 omitted["database"] 782 .as_object_mut() 783 .expect("database") 784 .remove("max_connections"); 785 assert!(semantic_valid(&omitted, Profile::Production)); 786 } 787 788 #[test] 789 fn schema_version_authority_and_protected_material_are_closed() { 790 let value = example(); 791 for replacement in [json!("radroots.rhi.config.v2"), json!("rhi")] { 792 let mut invalid = value.clone(); 793 invalid["schema"] = replacement; 794 assert_rejected(&invalid, Profile::Production); 795 } 796 for replacement in [json!(0), json!(2), json!("1")] { 797 let mut invalid = value.clone(); 798 invalid["schema_version"] = replacement; 799 assert_rejected(&invalid, Profile::Production); 800 } 801 for required in [ 802 "minimum_free_bytes", 803 "identity", 804 "relays", 805 "evidence", 806 "sources", 807 "deadline_ms", 808 "lookback_seconds", 809 "overlap_seconds", 810 "reconciliation", 811 "attestation", 812 "publication", 813 "presence", 814 "resource_limits", 815 "retention", 816 ] { 817 assert!( 818 CONFIG_SCHEMA.contains(required), 819 "missing authority {required}" 820 ); 821 } 822 let lowercase = CONFIG_EXAMPLE.to_ascii_lowercase(); 823 for forbidden in [ 824 "private_key", 825 "secret_key", 826 "mnemonic", 827 "nsec1", 828 "wrapping_key =", 829 "wrapping_key_path", 830 "rhi_", 831 "env_file", 832 "plaintext_file", 833 "keyring", 834 "managed_account", 835 "worker_root", 836 "state_file", 837 "json_state", 838 ] { 839 assert!( 840 !lowercase.contains(forbidden), 841 "forbidden material {forbidden}" 842 ); 843 } 844 }