services_hardening_state_metadata.rs (8193B)
1 #![forbid(unsafe_code)] 2 3 use std::{error::Error, path::Path}; 4 5 use myc::{ 6 MYC_CONFIG_SCHEMA_VERSION, MYC_OPERATOR_CONTRACT_VERSION, MYC_SIGNER_STATUS_CONTRACT_VERSION, 7 MYC_STATE_APPLICATION_ID, MYC_STATE_BASE_SCHEMA_VERSION, MYC_STATE_SCHEMA_VERSION, 8 MycConfigProfile, MycStateMetadata, MycStateMetadataErrorKind, RadrootsHostEnvironment, 9 RadrootsPathResolver, RadrootsPlatform, parse_myc_cli_v1_from, parse_myc_config_v1, 10 resolve_myc_runtime_context, 11 }; 12 use radroots_storage::event::SourceGeneration; 13 14 const EXAMPLE: &str = include_str!("../contracts/services_hardening/config.v1.example.toml"); 15 const LIB_SOURCE: &str = include_str!("../src/lib.rs"); 16 const METADATA_SOURCE: &str = include_str!("../src/state_metadata.rs"); 17 18 fn runtime(root: &Path, profile: &str) -> myc::MycRuntimeContext { 19 let root = root.to_str().expect("UTF-8 temporary root"); 20 let invocation = parse_myc_cli_v1_from([ 21 "myc", 22 "--profile", 23 profile, 24 "--instance", 25 "primary", 26 "--repo-local-root", 27 root, 28 "run", 29 ]) 30 .expect("valid test invocation"); 31 resolve_myc_runtime_context( 32 &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()), 33 &invocation, 34 ) 35 .expect("runtime context") 36 } 37 38 fn state_metadata( 39 runtime: &myc::MycRuntimeContext, 40 source: &str, 41 ) -> Result<MycStateMetadata, myc::MycStateMetadataError> { 42 let configuration = 43 parse_myc_config_v1(source.as_bytes(), MycConfigProfile::RepoLocal).expect("configuration"); 44 MycStateMetadata::new( 45 runtime, 46 &configuration, 47 SourceGeneration::new([0x5a; 32]).expect("generation"), 48 1_725_000_000_000, 49 ) 50 } 51 52 #[test] 53 fn exact_database_configuration_identity_and_policy_bindings_are_frozen() { 54 let directory = tempfile::tempdir().expect("temporary root"); 55 let runtime = runtime(directory.path(), "repo-local"); 56 let metadata = state_metadata(&runtime, EXAMPLE).expect("state metadata"); 57 let database = metadata.initial_database_metadata(); 58 59 assert_eq!(MYC_STATE_APPLICATION_ID.to_be_bytes(), *b"RDMY"); 60 assert_eq!(database.application_id().get(), MYC_STATE_APPLICATION_ID); 61 assert_eq!(database.service().as_str(), "myc"); 62 assert_eq!(database.instance().as_str(), "primary"); 63 assert_eq!(database.source_generation().as_bytes(), &[0x5a; 32]); 64 assert_eq!( 65 database.state_schema_version().get(), 66 MYC_STATE_BASE_SCHEMA_VERSION 67 ); 68 assert_eq!(database.created_at_unix_ms(), 1_725_000_000_000); 69 assert_eq!( 70 metadata 71 .database_identity() 72 .supported_state_schema_version() 73 .get(), 74 MYC_STATE_SCHEMA_VERSION 75 ); 76 77 let identities = metadata.expected_identities(); 78 assert_eq!(identities.transport().as_hex(), "4".repeat(64)); 79 assert_eq!(identities.user().as_hex(), "2".repeat(64)); 80 assert_eq!( 81 identities.discovery().expect("discovery identity").as_hex(), 82 "3".repeat(64) 83 ); 84 85 let versions = metadata.policy_versions(); 86 assert_eq!(versions.configuration(), MYC_CONFIG_SCHEMA_VERSION); 87 assert_eq!(versions.state(), MYC_STATE_SCHEMA_VERSION); 88 assert_eq!(versions.operator(), MYC_OPERATOR_CONTRACT_VERSION); 89 assert_eq!(versions.status(), MYC_SIGNER_STATUS_CONTRACT_VERSION); 90 assert_eq!( 91 hex::encode(metadata.configuration_digest().as_bytes()), 92 "68f65b32652a4646d33aafc12075ff44548fb7d9f9a96c4eacaa823702d386cc" 93 ); 94 } 95 96 #[test] 97 fn digest_uses_fully_defaulted_values_and_changes_with_normalized_policy() { 98 let directory = tempfile::tempdir().expect("temporary root"); 99 let runtime = runtime(directory.path(), "repo-local"); 100 let explicit = state_metadata(&runtime, EXAMPLE).expect("explicit defaults"); 101 let implicit_source = EXAMPLE 102 .replace("shutdown_grace_ms = 30000\n", "") 103 .replace("level = \"info\"\n", "") 104 .replace("format = \"json\"\n", "") 105 .replace("busy_timeout_ms = 5000\n", "") 106 .replace("max_connections = 8\n", "") 107 .replace( 108 "[resource_limits.runtime]\nworker_threads = 4\nblocking_threads = 8\n\n", 109 "", 110 ); 111 let implicit = state_metadata(&runtime, &implicit_source).expect("implicit defaults"); 112 assert_eq!( 113 explicit.configuration_digest(), 114 implicit.configuration_digest() 115 ); 116 117 let changed = state_metadata( 118 &runtime, 119 &EXAMPLE.replace("level = \"info\"", "level = \"warn\""), 120 ) 121 .expect("changed policy"); 122 assert_ne!( 123 explicit.configuration_digest(), 124 changed.configuration_digest() 125 ); 126 127 let changed_ingress = state_metadata( 128 &runtime, 129 &EXAMPLE.replacen( 130 "maximum_past_seconds = 120", 131 "maximum_past_seconds = 121", 132 1, 133 ), 134 ) 135 .expect("changed ingress policy"); 136 assert_ne!( 137 explicit.configuration_digest(), 138 changed_ingress.configuration_digest() 139 ); 140 } 141 142 #[test] 143 fn disabled_discovery_binds_no_discovery_identity() { 144 let directory = tempfile::tempdir().expect("temporary root"); 145 let runtime = runtime(directory.path(), "repo-local"); 146 let mut document = toml::from_str::<toml::Value>(EXAMPLE).expect("example TOML"); 147 document["identity"]["discovery"] = toml::Value::Table( 148 [("enabled".to_owned(), toml::Value::Boolean(false))] 149 .into_iter() 150 .collect(), 151 ); 152 document["discovery"] = toml::Value::Table( 153 [("enabled".to_owned(), toml::Value::Boolean(false))] 154 .into_iter() 155 .collect(), 156 ); 157 let source = toml::to_string(&document).expect("disabled-discovery TOML"); 158 let metadata = state_metadata(&runtime, &source).expect("state metadata"); 159 160 assert!(metadata.expected_identities().discovery().is_none()); 161 } 162 163 #[test] 164 fn profile_and_invalid_nostr_identity_bindings_fail_closed() { 165 let directory = tempfile::tempdir().expect("temporary root"); 166 let runtime = runtime(directory.path(), "repo-local"); 167 let production = parse_myc_config_v1(EXAMPLE.as_bytes(), MycConfigProfile::Production) 168 .expect("production configuration"); 169 let error = MycStateMetadata::new( 170 &runtime, 171 &production, 172 SourceGeneration::new([0x5a; 32]).expect("generation"), 173 1, 174 ) 175 .expect_err("profile mismatch"); 176 assert_eq!(error.kind(), MycStateMetadataErrorKind::Profile); 177 assert!(Error::source(&error).is_none()); 178 179 let invalid_role = EXAMPLE.replacen(&"4".repeat(64), &"f".repeat(64), 1); 180 assert!( 181 parse_myc_config_v1(invalid_role.as_bytes(), MycConfigProfile::RepoLocal).is_err(), 182 "an out-of-field role identity must not remain a valid configuration" 183 ); 184 let invalid_client = EXAMPLE.replacen(&"7".repeat(64), &"f".repeat(64), 1); 185 assert!( 186 parse_myc_config_v1(invalid_client.as_bytes(), MycConfigProfile::RepoLocal).is_err(), 187 "an out-of-field policy identity must not remain a valid configuration" 188 ); 189 } 190 191 #[test] 192 fn metadata_debug_error_and_package_boundary_disclose_no_values() { 193 let directory = tempfile::tempdir().expect("temporary root"); 194 let runtime = runtime(directory.path(), "repo-local"); 195 let metadata = state_metadata(&runtime, EXAMPLE).expect("state metadata"); 196 let rendered = format!("{metadata:?}"); 197 for forbidden in [ 198 directory.path().to_string_lossy().as_ref(), 199 &"4".repeat(64), 200 &"2".repeat(64), 201 &"3".repeat(64), 202 &hex::encode(metadata.configuration_digest().as_bytes()), 203 ] { 204 assert!(!rendered.contains(forbidden)); 205 } 206 207 assert!(LIB_SOURCE.contains("mod state_metadata;")); 208 assert!(!LIB_SOURCE.contains("pub mod state_metadata;")); 209 for forbidden in [ 210 "sqlx::", 211 "rusqlite", 212 "CREATE TABLE", 213 "INSERT INTO", 214 "UPDATE ", 215 "DELETE FROM", 216 "std::fs", 217 "std::env", 218 "std::time", 219 "Serialize", 220 "Deserialize", 221 ] { 222 assert!( 223 !METADATA_SOURCE.contains(forbidden), 224 "found forbidden metadata authority `{forbidden}`" 225 ); 226 } 227 }