services_hardening_contracts.rs (19804B)
1 #![forbid(unsafe_code)] 2 3 use serde_json::Value; 4 use sha2::{Digest, Sha256}; 5 use std::collections::BTreeSet; 6 7 const OPERATOR_CONTRACT: &str = 8 include_str!("../contracts/services_hardening/operator_contract.v1.json"); 9 const CONSUMER_ROOT: &str = include_str!("../.radroots-consumer-root"); 10 11 fn contract() -> Value { 12 serde_json::from_str(OPERATOR_CONTRACT).expect("operator contract must be valid JSON") 13 } 14 15 fn decision_sections_digest(value: &Value) -> String { 16 let sections = serde_json::json!({ 17 "identity_contract": value["admin"]["identity_contract"], 18 "model_wire_contract": value["admin"]["model_wire_contract"], 19 "models": value["admin"]["models"], 20 "mutation_contract": value["admin"]["mutation_contract"], 21 "pagination": value["admin"]["pagination"], 22 "path_parameters": value["admin"]["path_parameters"], 23 "types": value["admin"]["types"] 24 }); 25 hex::encode(Sha256::digest( 26 serde_json::to_vec(§ions).expect("serialize decision sections"), 27 )) 28 } 29 30 fn permission_vector_is_valid(value: &str) -> bool { 31 if matches!( 32 value, 33 "nip04_decrypt" | "nip04_encrypt" | "nip44_decrypt" | "nip44_encrypt" | "switch_relays" 34 ) { 35 return true; 36 } 37 let Some(kind) = value.strip_prefix("sign_event:kind:") else { 38 return false; 39 }; 40 if kind.is_empty() || (kind.len() > 1 && kind.starts_with('0')) { 41 return false; 42 } 43 kind.bytes().all(|byte| byte.is_ascii_digit()) && kind.parse::<u32>().is_ok() 44 } 45 46 #[test] 47 fn source_lock_identity_and_shared_host_reference_are_exact() { 48 assert_eq!(CONSUMER_ROOT, "myc\n"); 49 let value = contract(); 50 assert_eq!(value["schema"], "radroots.myc.operator-contract.v1"); 51 assert_eq!(value["contract_version"], 1); 52 assert_eq!(value["decision_state"], "implemented"); 53 assert_eq!(value["service"], "myc"); 54 assert_eq!( 55 value["shared_host_contract"], 56 serde_json::json!({ 57 "repository": "https://github.com/radrootslabs/lib", 58 "path": "contracts/architecture/decisions/services_hardening_host.v1.json", 59 "schema": "radroots.services-hardening.host-decisions.v1", 60 "contract_version": 1 61 }) 62 ); 63 } 64 65 #[test] 66 fn admin_inventory_is_closed_unique_and_model_complete() { 67 let value = contract(); 68 assert_eq!( 69 value["admin"]["transport"], 70 "http_1_1_over_unix_domain_socket" 71 ); 72 assert_eq!(value["admin"]["base_path"], "/v1"); 73 assert_eq!(value["admin"]["route_inventory_closed"], true); 74 75 let routes = value["admin"]["routes"].as_array().expect("routes"); 76 let exact_routes = routes 77 .iter() 78 .map(|route| { 79 format!( 80 "{}|{}|{}|{}|{}|{}", 81 route["method"].as_str().unwrap(), 82 route["path"].as_str().unwrap(), 83 route["operation_id"].as_str().unwrap(), 84 route["request_model"].as_str().unwrap(), 85 route["response_model"].as_str().unwrap(), 86 route["mutation"].as_bool().unwrap() 87 ) 88 }) 89 .collect::<Vec<_>>(); 90 assert_eq!( 91 exact_routes, 92 [ 93 "GET|/v1/status|radroots.myc.status.get.v1|empty|service_status_v1|false", 94 "GET|/v1/config/effective|radroots.myc.config.effective.get.v1|empty|effective_config_v1|false", 95 "GET|/v1/identity/status|radroots.myc.identity.status.get.v1|identity_status_query_v1|identity_status_v1|false", 96 "GET|/v1/identity/public|radroots.myc.identity.public.get.v1|identity_public_query_v1|identity_public_v1|false", 97 "GET|/v1/state/status|radroots.myc.state.status.get.v1|empty|state_status_v1|false", 98 "POST|/v1/state/backup|radroots.myc.state.backup.create.v1|state_backup_request_v1|state_backup_receipt_v1|true", 99 "GET|/v1/metrics/snapshot|radroots.myc.metrics.snapshot.get.v1|empty|metrics_snapshot_v1|false", 100 "GET|/v1/connections|radroots.myc.connections.list.v1|connections_query_v1|connections_page_v1|false", 101 "POST|/v1/connections/{connection_id}/approve|radroots.myc.connection.approve.v1|connection_approve_request_v1|connection_mutation_receipt_v1|true", 102 "POST|/v1/connections/{connection_id}/reject|radroots.myc.connection.reject.v1|connection_reject_request_v1|connection_mutation_receipt_v1|true", 103 "POST|/v1/connections/{connection_id}/revoke|radroots.myc.connection.revoke.v1|connection_revoke_request_v1|connection_mutation_receipt_v1|true", 104 "POST|/v1/authorization/challenges/require|radroots.myc.authorization.challenge.require.v1|challenge_require_request_v1|challenge_v1|true", 105 "POST|/v1/authorization/challenges/{challenge_id}/authorize|radroots.myc.authorization.challenge.authorize.v1|challenge_authorize_request_v1|challenge_authorization_receipt_v1|true", 106 "GET|/v1/audit/events|radroots.myc.audit.events.list.v1|audit_events_query_v1|audit_events_page_v1|false", 107 "GET|/v1/audit/summary|radroots.myc.audit.summary.get.v1|audit_summary_query_v1|audit_summary_v1|false", 108 "GET|/v1/discovery/desired|radroots.myc.discovery.desired.get.v1|empty|discovery_desired_v1|false", 109 "POST|/v1/discovery/render|radroots.myc.discovery.render.v1|discovery_render_request_v1|discovery_render_receipt_v1|true", 110 "POST|/v1/discovery/refresh|radroots.myc.discovery.refresh.v1|discovery_refresh_request_v1|discovery_refresh_receipt_v1|true", 111 "POST|/v1/discovery/publish|radroots.myc.discovery.publish.v1|discovery_publish_request_v1|discovery_publish_receipt_v1|true" 112 ] 113 ); 114 let route_keys = routes 115 .iter() 116 .map(|route| format!("{} {}", route["method"], route["path"])) 117 .collect::<BTreeSet<_>>(); 118 let operation_ids = routes 119 .iter() 120 .map(|route| route["operation_id"].as_str().expect("operation ID")) 121 .collect::<BTreeSet<_>>(); 122 assert_eq!(route_keys.len(), routes.len()); 123 assert_eq!(operation_ids.len(), routes.len()); 124 assert!( 125 operation_ids 126 .iter() 127 .all(|id| id.starts_with("radroots.myc.") && id.ends_with(".v1")) 128 ); 129 130 let models = value["admin"]["models"].as_object().expect("models"); 131 let types = value["admin"]["types"].as_object().expect("types"); 132 for route in routes { 133 for key in ["request_model", "response_model"] { 134 let model = route[key].as_str().expect("model reference"); 135 assert!(models.contains_key(model), "missing model {model}"); 136 } 137 assert_eq!(route["mutation"], route["method"] == "POST"); 138 } 139 for (model_name, model) in models { 140 assert_eq!( 141 model 142 .as_object() 143 .unwrap() 144 .keys() 145 .map(String::as_str) 146 .collect::<Vec<_>>(), 147 ["fields"], 148 "model {model_name} must be a closed field inventory" 149 ); 150 for (field_name, field) in model["fields"].as_object().unwrap() { 151 assert_eq!( 152 field 153 .as_object() 154 .unwrap() 155 .keys() 156 .map(String::as_str) 157 .collect::<Vec<_>>(), 158 ["presence", "type"], 159 "field {model_name}.{field_name} must bind only type and presence" 160 ); 161 assert!( 162 matches!(field["presence"].as_str(), Some("required" | "optional")), 163 "invalid presence for {model_name}.{field_name}" 164 ); 165 let type_name = field["type"].as_str().unwrap(); 166 assert!(types.contains_key(type_name), "unknown type {type_name}"); 167 } 168 } 169 for (type_name, descriptor) in types { 170 let referenced = match descriptor["kind"].as_str().unwrap() { 171 "array" | "canonical_delimited_set" => { 172 vec![descriptor["items"].as_str().unwrap()] 173 } 174 "map" => vec![ 175 descriptor["key"].as_str().unwrap(), 176 descriptor["value"].as_str().unwrap(), 177 ], 178 "closed_object" => descriptor["fields"] 179 .as_object() 180 .unwrap() 181 .values() 182 .map(|field| field.as_str().unwrap()) 183 .collect(), 184 "tagged_union" => descriptor["variants"] 185 .as_array() 186 .unwrap() 187 .iter() 188 .map(|variant| variant.as_str().unwrap()) 189 .collect(), 190 "alias" => vec![descriptor["target"].as_str().unwrap()], 191 "optional" => vec![descriptor["value"].as_str().unwrap()], 192 "boolean" 193 | "canonical_json_object" 194 | "enum" 195 | "integer" 196 | "literal" 197 | "string" 198 | "string_union" => Vec::new(), 199 kind => panic!("unknown descriptor kind {kind} for {type_name}"), 200 }; 201 for reference in referenced { 202 assert!( 203 types.contains_key(reference), 204 "type {type_name} references missing type {reference}" 205 ); 206 } 207 } 208 assert_eq!( 209 value["admin"]["identity_contract"]["roles"], 210 serde_json::json!([ 211 { "id": "transport", "required": true, "disabled_allowed": false, "providers": ["encrypted_file", "local_signer"] }, 212 { "id": "user", "required": true, "disabled_allowed": false, "providers": ["encrypted_file", "local_signer"] }, 213 { "id": "discovery", "required": false, "disabled_allowed": true, "providers": ["encrypted_file", "local_signer"] } 214 ]) 215 ); 216 assert_eq!( 217 value["admin"]["types"]["service_phase"], 218 serde_json::json!({ 219 "kind": "enum", 220 "values": ["starting", "ready", "degraded", "unready", "stopping", "failed"] 221 }) 222 ); 223 assert_eq!( 224 value["admin"]["types"]["nip46_permission"], 225 serde_json::json!({ 226 "kind": "string_union", 227 "simple_values": ["nip04_decrypt", "nip04_encrypt", "nip44_decrypt", "nip44_encrypt", "switch_relays"], 228 "sign_event_pattern": "^sign_event:kind:(0|[1-9][0-9]{0,9})$", 229 "sign_event_kind_minimum": 0, 230 "sign_event_kind_maximum": 4_294_967_295_u64, 231 "sign_event_kind_encoding": "canonical_unsigned_decimal_no_leading_zeroes", 232 "maximum_utf8_bytes": 64, 233 "unpermissioned_methods": ["connect", "get_public_key", "get_session_capability", "ping", "logout"], 234 "custom_methods": "unsupported_v1", 235 "required_permission_api": "radroots_nostr_connect::server::required_permission", 236 "permission_type": "radroots_nostr_connect::Permission", 237 "fixed_vectors": { 238 "valid": ["sign_event:kind:0", "sign_event:kind:1", "sign_event:kind:4294967295", "switch_relays"], 239 "invalid": ["get_public_key", "ping", "sign_event", "sign_event:kind:", "sign_event:kind:00", "sign_event:kind:4294967296", "sign_event:kind:9999999999"] 240 } 241 }) 242 ); 243 for vector in value["admin"]["types"]["nip46_permission"]["fixed_vectors"]["valid"] 244 .as_array() 245 .unwrap() 246 { 247 assert!(permission_vector_is_valid(vector.as_str().unwrap())); 248 } 249 for vector in value["admin"]["types"]["nip46_permission"]["fixed_vectors"]["invalid"] 250 .as_array() 251 .unwrap() 252 { 253 assert!(!permission_vector_is_valid(vector.as_str().unwrap())); 254 } 255 assert_eq!( 256 value["admin"]["models"]["service_status_v1"]["fields"] 257 .as_object() 258 .unwrap() 259 .keys() 260 .map(String::as_str) 261 .collect::<Vec<_>>(), 262 [ 263 "build_info", 264 "configuration", 265 "contract_version", 266 "instance", 267 "myc", 268 "persistence", 269 "phase", 270 "provider", 271 "ready", 272 "reason_codes", 273 "service", 274 "transport", 275 "uptime_millis" 276 ] 277 ); 278 assert_eq!( 279 value["admin"]["identity_contract"], 280 serde_json::json!({ 281 "roles": [ 282 { "id": "transport", "required": true, "disabled_allowed": false, "providers": ["encrypted_file", "local_signer"] }, 283 { "id": "user", "required": true, "disabled_allowed": false, "providers": ["encrypted_file", "local_signer"] }, 284 { "id": "discovery", "required": false, "disabled_allowed": true, "providers": ["encrypted_file", "local_signer"] } 285 ], 286 "live_mutation": false, 287 "rotation_mode": "offline_create_new_then_config_apply", 288 "in_place_overwrite": false 289 }) 290 ); 291 assert_eq!( 292 value["admin"]["path_parameters"], 293 serde_json::json!({ 294 "connection_id": { "type": "bounded_id", "source": "percent_decoded_single_path_segment", "slash_allowed": false }, 295 "challenge_id": { "type": "bounded_id", "source": "percent_decoded_single_path_segment", "slash_allowed": false } 296 }) 297 ); 298 assert_eq!( 299 value["admin"]["pagination"], 300 serde_json::json!({ 301 "cursor_type": "page_cursor", 302 "limit_min": 1, 303 "limit_max": 200, 304 "terminal_page": "next_cursor_field_absent", 305 "cursor_reuse": "same_route_same_filters_only", 306 "filter_or_route_mismatch": "invalid_cursor", 307 "connections_order": ["created_at_utc_ascending", "connection_id_ascending"], 308 "connections_snapshot": "generation_fixed_by_first_page_cursor", 309 "audit_order": ["occurred_at_utc_descending", "audit_id_descending"], 310 "audit_snapshot": "maximum_audit_sequence_fixed_by_first_page_cursor" 311 }) 312 ); 313 314 let mutation_operations = routes 315 .iter() 316 .filter(|route| route["mutation"] == true) 317 .map(|route| route["operation_id"].as_str().unwrap()) 318 .collect::<BTreeSet<_>>(); 319 let committed_effects = value["admin"]["mutation_contract"]["committed_effects"] 320 .as_object() 321 .unwrap() 322 .keys() 323 .map(String::as_str) 324 .collect::<BTreeSet<_>>(); 325 assert_eq!(mutation_operations, committed_effects); 326 assert_eq!( 327 decision_sections_digest(&value), 328 "4967401cbb7b777aa78e19e91c75fae9d1245a307bf37ea30611d30d8ffeebbd" 329 ); 330 } 331 332 #[test] 333 fn doctor_exit_and_tcp_contracts_are_exact() { 334 let value = contract(); 335 assert_eq!( 336 value["doctor"]["shared_schema"], 337 "radroots.service.doctor.v1" 338 ); 339 assert_eq!(value["doctor"]["contract_version"], 1); 340 assert_eq!(value["doctor"]["execution"], "ordered"); 341 assert_eq!(value["doctor"]["pass_requires_all_scope"], true); 342 assert_eq!( 343 value["doctor"]["probe_future_cancellation"], 344 "drop_stops_or_owns_cleanup" 345 ); 346 assert_eq!(value["doctor"]["detached_probe_work"], false); 347 assert_eq!( 348 value["doctor"]["statuses"], 349 serde_json::json!(["pass", "fail", "timeout", "skipped"]) 350 ); 351 assert_eq!( 352 value["doctor"]["aggregate_statuses"], 353 serde_json::json!(["pass", "degraded", "fail"]) 354 ); 355 assert_eq!(value["doctor"]["required_skipped"], "forbidden"); 356 assert_eq!(value["doctor"]["summary_max_utf8_bytes"], 256); 357 assert_eq!(value["doctor"]["report_max_utf8_bytes"], 8192); 358 assert_eq!(value["doctor"]["raw_error_or_path_allowed"], false); 359 assert_eq!(value["doctor"]["required_fail_or_timeout_exit"], 6); 360 assert_eq!( 361 value["doctor"]["checks"], 362 serde_json::json!([ 363 { "id": "paths_permissions", "required": true, "deadline_ms": 2000, "remediation_code": "correct_path_policy", "scope": ["resolved_path_containment", "owner", "type", "mode"] }, 364 { "id": "writer_lock", "required": true, "deadline_ms": 2000, "remediation_code": "release_writer_lock", "scope": ["state_directory_binding", "writer_lock_state"] }, 365 { "id": "sqlite_schema", "required": true, "deadline_ms": 5000, "remediation_code": "repair_schema", "scope": ["metadata_identity", "migration_history", "schema_catalog"] }, 366 { "id": "sqlite_integrity", "required": true, "deadline_ms": 15000, "remediation_code": "restore_verified_state", "scope": ["integrity_check", "foreign_key_check"] }, 367 { "id": "sqlite_free_space", "required": true, "deadline_ms": 2000, "remediation_code": "free_state_disk_space", "scope": ["state_filesystem_capacity", "minimum_free_bytes"] }, 368 { "id": "identity_binding", "required": true, "deadline_ms": 2000, "remediation_code": "restore_identity_binding", "scope": ["envelope_contract", "credential_reference", "public_identity"] }, 369 { "id": "signer_provider", "required": true, "deadline_ms": 15000, "remediation_code": "repair_signer_provider", "scope": ["capability", "contract_version", "identity", "correlation", "deadline"] }, 370 { "id": "admin_bind_policy", "required": true, "deadline_ms": 2000, "remediation_code": "correct_admin_bind_policy", "scope": ["unix_socket_path", "socket_mode", "peer_authorization"] }, 371 { "id": "operations_bind_policy", "required": true, "deadline_ms": 2000, "remediation_code": "correct_operations_bind_policy", "scope": ["enabled_posture", "listen_address", "bind_policy"] }, 372 { "id": "network_policy", "required": true, "deadline_ms": 2000, "remediation_code": "correct_network_policy", "scope": ["dns_policy", "tls_policy", "relay_url_policy"] }, 373 { "id": "required_relays", "required": true, "deadline_ms": 15000, "remediation_code": "restore_required_relays", "scope": ["required_read_relays", "required_write_relays", "connect_deadline"] }, 374 { "id": "outbox_invariants", "required": true, "deadline_ms": 5000, "remediation_code": "repair_outbox_state", "scope": ["claim_invariants", "retry_state", "exact_response_bytes"] }, 375 { "id": "clock_skew", "required": false, "deadline_ms": 5000, "remediation_code": "correct_clock", "scope": ["wall_clock_skew"] } 376 ]) 377 ); 378 assert_eq!( 379 value["exit_codes"], 380 serde_json::json!([ 381 { "code": 0, "name": "success", "meaning": "successful command or completed graceful first-signal shutdown" }, 382 { "code": 1, "name": "unexpected_internal", "meaning": "unexpected invariant, critical task, or internal failure" }, 383 { "code": 2, "name": "input_or_configuration", "meaning": "CLI, config, validation, or unsupported contract input" }, 384 { "code": 3, "name": "service_or_dependency_unavailable", "meaning": "daemon, required provider, relay, source, or local dependency unavailable" }, 385 { "code": 4, "name": "state_or_identity_unavailable", "meaning": "state, schema, lock, credential, or identity unavailable" }, 386 { "code": 5, "name": "operation_rejected_or_conflict", "meaning": "authorization rejection, idempotency conflict, stale generation, or domain conflict" }, 387 { "code": 6, "name": "doctor_required_check_failed", "meaning": "one or more required doctor checks failed or timed out" } 388 ]) 389 ); 390 assert_eq!( 391 value["tcp_operations"], 392 serde_json::json!({ 393 "routes": [ 394 { "method": "GET", "path": "/livez", "source": "cached_supervisor_state" }, 395 { "method": "GET", "path": "/readyz", "source": "cached_readiness_state" }, 396 { "method": "GET", "path": "/metrics", "source": "cached_bounded_metrics_snapshot" } 397 ], 398 "active_probe_per_request": false, 399 "additional_routes": false 400 }) 401 ); 402 }