services_hardening_state_metadata.rs (7055B)
1 #![forbid(unsafe_code)] 2 3 use std::{error::Error, path::Path}; 4 5 use radroots_storage::event::SourceGeneration; 6 use rhi::{ 7 RHI_ADMIN_CONTRACT_VERSION, RHI_CONFIG_SCHEMA_VERSION, RHI_PROVIDER_CONTRACT_VERSION, 8 RHI_STATE_APPLICATION_ID, RHI_STATE_BASE_SCHEMA_VERSION, RHI_STATE_SCHEMA_VERSION, 9 RHI_STATUS_CONTRACT_VERSION, RadrootsHostEnvironment, RadrootsPathResolver, RadrootsPlatform, 10 RhiConfigProfile, RhiStateMetadata, RhiStateMetadataErrorKind, parse_rhi_cli_v1_from, 11 parse_rhi_config_v1, resolve_rhi_runtime_context, 12 }; 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) -> rhi::RhiRuntimeContext { 19 let root = root.to_str().expect("UTF-8 temporary root"); 20 let invocation = parse_rhi_cli_v1_from([ 21 "rhi", 22 "--profile", 23 profile, 24 "--instance", 25 "primary", 26 "--repo-local-root", 27 root, 28 "run", 29 ]) 30 .expect("valid test invocation"); 31 resolve_rhi_runtime_context( 32 &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()), 33 &invocation, 34 ) 35 .expect("runtime context") 36 } 37 38 fn state_metadata( 39 runtime: &rhi::RhiRuntimeContext, 40 source: &str, 41 ) -> Result<RhiStateMetadata, rhi::RhiStateMetadataError> { 42 let configuration = 43 parse_rhi_config_v1(source.as_bytes(), RhiConfigProfile::RepoLocal).expect("configuration"); 44 RhiStateMetadata::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.database(); 58 59 assert_eq!(RHI_STATE_APPLICATION_ID.to_be_bytes(), *b"RDRH"); 60 assert_eq!(database.application_id().get(), RHI_STATE_APPLICATION_ID); 61 assert_eq!(database.service().as_str(), "rhi"); 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 RHI_STATE_BASE_SCHEMA_VERSION 67 ); 68 assert_eq!( 69 metadata 70 .database_identity() 71 .supported_state_schema_version() 72 .get(), 73 RHI_STATE_SCHEMA_VERSION 74 ); 75 assert_eq!(database.created_at_unix_ms(), 1_725_000_000_000); 76 assert_eq!(metadata.expected_identity().as_hex(), "2".repeat(64)); 77 78 let versions = metadata.policy_versions(); 79 assert_eq!(versions.configuration(), RHI_CONFIG_SCHEMA_VERSION); 80 assert_eq!(versions.state(), RHI_STATE_SCHEMA_VERSION); 81 assert_eq!(versions.admin(), RHI_ADMIN_CONTRACT_VERSION); 82 assert_eq!(versions.status(), RHI_STATUS_CONTRACT_VERSION); 83 assert_eq!(versions.provider(), RHI_PROVIDER_CONTRACT_VERSION); 84 assert_eq!( 85 lower_hex(metadata.configuration_digest().as_bytes()), 86 "7950e77614e1302f673d3434a58a69bfb4ce9c8006b61b29a12deb2b729136d4" 87 ); 88 assert_eq!( 89 lower_hex(metadata.evidence_policy_digest().as_bytes()), 90 "43f083e29fcff4f90e66b546c49f74b0205ecb148beb352adb5a211d3e5f86b2" 91 ); 92 } 93 94 #[test] 95 fn digests_use_fully_defaulted_values_and_change_with_normalized_policy() { 96 let directory = tempfile::tempdir().expect("temporary root"); 97 let runtime = runtime(directory.path(), "repo-local"); 98 let explicit = state_metadata(&runtime, EXAMPLE).expect("explicit defaults"); 99 let implicit_source = EXAMPLE 100 .replace("shutdown_grace_ms = 30000\n", "") 101 .replace("level = \"info\"\n", "") 102 .replace("format = \"json\"\n", "") 103 .replace("busy_timeout_ms = 5000\n", "") 104 .replace("max_connections = 8\n", ""); 105 let implicit = state_metadata(&runtime, &implicit_source).expect("implicit defaults"); 106 assert_eq!( 107 explicit.configuration_digest(), 108 implicit.configuration_digest() 109 ); 110 assert_eq!( 111 explicit.evidence_policy_digest(), 112 implicit.evidence_policy_digest() 113 ); 114 115 let changed = state_metadata( 116 &runtime, 117 &EXAMPLE.replace("deadline_ms = 10000", "deadline_ms = 10001"), 118 ) 119 .expect("changed policy"); 120 assert_ne!( 121 explicit.configuration_digest(), 122 changed.configuration_digest() 123 ); 124 assert_ne!( 125 explicit.evidence_policy_digest(), 126 changed.evidence_policy_digest() 127 ); 128 } 129 130 #[test] 131 fn profile_binding_fails_closed_without_state_or_source_disclosure() { 132 let directory = tempfile::tempdir().expect("temporary root"); 133 let runtime = runtime(directory.path(), "repo-local"); 134 let production = parse_rhi_config_v1(EXAMPLE.as_bytes(), RhiConfigProfile::Production) 135 .expect("production configuration"); 136 let error = RhiStateMetadata::new( 137 &runtime, 138 &production, 139 SourceGeneration::new([0x5a; 32]).expect("generation"), 140 1, 141 ) 142 .expect_err("profile mismatch"); 143 assert_eq!(error.kind(), RhiStateMetadataErrorKind::Profile); 144 assert!(Error::source(&error).is_none()); 145 let rendered = format!("{error} {error:?}"); 146 assert!(!rendered.contains(directory.path().to_string_lossy().as_ref())); 147 assert!(!rendered.contains(&"2".repeat(64))); 148 } 149 150 #[test] 151 fn metadata_debug_and_package_boundary_disclose_no_values() { 152 let directory = tempfile::tempdir().expect("temporary root"); 153 let runtime = runtime(directory.path(), "repo-local"); 154 let metadata = state_metadata(&runtime, EXAMPLE).expect("state metadata"); 155 let rendered = format!("{metadata:?}"); 156 for forbidden in [ 157 directory.path().to_string_lossy().as_ref(), 158 &"2".repeat(64), 159 &lower_hex(metadata.configuration_digest().as_bytes()), 160 &lower_hex(metadata.evidence_policy_digest().as_bytes()), 161 ] { 162 assert!(!rendered.contains(forbidden)); 163 } 164 165 assert!(LIB_SOURCE.contains("mod state_metadata;")); 166 assert!(!LIB_SOURCE.contains("pub mod state_metadata;")); 167 for forbidden in [ 168 "sqlx::", 169 "rusqlite", 170 "CREATE TABLE", 171 "INSERT INTO", 172 "UPDATE ", 173 "DELETE FROM", 174 "std::fs", 175 "std::env", 176 "std::time", 177 "Serialize", 178 "Deserialize", 179 ] { 180 assert!( 181 !METADATA_SOURCE.contains(forbidden), 182 "found forbidden metadata authority `{forbidden}`" 183 ); 184 } 185 } 186 187 fn lower_hex(bytes: &[u8]) -> String { 188 const DIGITS: &[u8; 16] = b"0123456789abcdef"; 189 let mut output = String::with_capacity(bytes.len() * 2); 190 for byte in bytes { 191 output.push(char::from(DIGITS[usize::from(byte >> 4)])); 192 output.push(char::from(DIGITS[usize::from(byte & 0x0f)])); 193 } 194 output 195 }