services_hardening_wave_100_b.rs (9339B)
1 #![forbid(unsafe_code)] 2 #![cfg(any(target_os = "linux", target_os = "macos"))] 3 4 use std::{fs, os::unix::fs::PermissionsExt, path::Path}; 5 6 use nostr::{Keys, SecretKey}; 7 use radroots_service_sqlite::{MigrationAppliedAtUnixSeconds, MigrationBuildIdentity}; 8 use radroots_storage::event::SourceGeneration; 9 use rhi::{ 10 RadrootsHostEnvironment, RadrootsPathResolver, RadrootsPlatform, RhiConfigProfile, 11 RhiEncryptedIdentityProvisioningMaterial, RhiIdentityEnvelopeBinding, RhiStateMetadata, 12 initialize_rhi_state, open_rhi_encrypted_identity, open_rhi_state_read_write, 13 parse_rhi_cli_v1_from, parse_rhi_config_v1, provision_rhi_encrypted_identity, 14 resolve_rhi_runtime_context, resolve_rhi_wrapping_credential, 15 }; 16 use sha2::{Digest, Sha256}; 17 18 const CONFIG_EXAMPLE: &str = include_str!("../contracts/services_hardening/config.v1.example.toml"); 19 const ENVELOPE_CONTRACT: &str = 20 include_str!("../contracts/services_hardening/encrypted_identity_envelope.v1.json"); 21 const CREDENTIAL_CONTRACT: &str = 22 include_str!("../contracts/services_hardening/wrapping_credential_resolution.v1.json"); 23 const CONFIG_SOURCE: &str = include_str!("../src/config_v1.rs"); 24 const CREDENTIAL_SOURCE: &str = include_str!("../src/identity_credential.rs"); 25 const ENVELOPE_SOURCE: &str = include_str!("../src/identity_envelope.rs"); 26 const STATE_HOST_SOURCE: &str = include_str!("../src/state_host.rs"); 27 const STATE_MAINTENANCE_SOURCE: &str = include_str!("../src/state_maintenance.rs"); 28 29 fn digest(label: &str) -> [u8; 32] { 30 Sha256::digest(label.as_bytes()).into() 31 } 32 33 fn identity_secret() -> [u8; 32] { 34 let mut candidate = digest("radroots.rhi.wave-100-b.identity-secret.v1"); 35 while SecretKey::from_slice(&candidate).is_err() { 36 candidate = Sha256::digest(candidate).into(); 37 } 38 candidate 39 } 40 41 fn runtime(root: &Path, instance: &str) -> rhi::RhiRuntimeContext { 42 let root = root.to_str().expect("UTF-8 temporary root"); 43 let invocation = parse_rhi_cli_v1_from([ 44 "rhi", 45 "--profile", 46 "repo-local", 47 "--instance", 48 instance, 49 "--repo-local-root", 50 root, 51 "run", 52 ]) 53 .expect("valid repo-local invocation"); 54 resolve_rhi_runtime_context( 55 &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()), 56 &invocation, 57 ) 58 .expect("runtime context") 59 } 60 61 fn configuration( 62 runtime: &rhi::RhiRuntimeContext, 63 expected_identity: &str, 64 ) -> rhi::RhiConfigDocumentV1 { 65 let source = CONFIG_EXAMPLE 66 .replace( 67 "/var/lib/radroots/services/rhi/default/secrets/service.identity.ncrypt", 68 runtime 69 .identity_path() 70 .to_str() 71 .expect("UTF-8 identity artifact path"), 72 ) 73 .replace(&"2".repeat(64), expected_identity); 74 parse_rhi_config_v1(source.as_bytes(), RhiConfigProfile::RepoLocal) 75 .expect("wave-two configuration") 76 } 77 78 fn prepare_secure_directory(path: &Path) { 79 fs::create_dir_all(path).expect("secure directory"); 80 fs::set_permissions(path, fs::Permissions::from_mode(0o700)).expect("secure mode"); 81 } 82 83 fn contains_bytes(haystack: &[u8], needle: &[u8]) -> bool { 84 !needle.is_empty() 85 && haystack 86 .windows(needle.len()) 87 .any(|window| window == needle) 88 } 89 90 fn migration_evidence() -> (MigrationAppliedAtUnixSeconds, MigrationBuildIdentity) { 91 let applied_at = MigrationAppliedAtUnixSeconds::new(1_725_000_000).expect("migration time"); 92 let build = MigrationBuildIdentity::new( 93 env!("CARGO_PKG_VERSION"), 94 "1111111111111111111111111111111111111111", 95 "053d0c750bf9cd683c6ea37cefe7e79617ba629f", 96 "rustc-test", 97 "test-target", 98 "service-host", 99 1, 100 rhi::RHI_STATE_SCHEMA_VERSION, 101 1, 102 1, 103 1, 104 ) 105 .expect("build identity"); 106 (applied_at, build) 107 } 108 109 #[tokio::test] 110 async fn wave_two_composes_one_runtime_without_crossing_secret_or_state_authority() { 111 let directory = tempfile::tempdir().expect("temporary root"); 112 let runtime = runtime(directory.path(), "primary"); 113 let secret = identity_secret(); 114 let expected_identity = Keys::new(SecretKey::from_slice(&secret).expect("identity secret")) 115 .public_key() 116 .to_hex(); 117 let configuration = configuration(&runtime, &expected_identity); 118 let metadata = RhiStateMetadata::new( 119 &runtime, 120 &configuration, 121 SourceGeneration::new([0x6b; 32]).expect("source generation"), 122 1_725_000_000_000, 123 ) 124 .expect("state metadata"); 125 let binding = RhiIdentityEnvelopeBinding::from_configuration(&configuration, &metadata) 126 .expect("identity binding"); 127 128 prepare_secure_directory(runtime.context().paths().secrets()); 129 let credential_bytes = digest("radroots.rhi.wave-100-b.wrapping-credential.v1"); 130 let credential_path = runtime 131 .context() 132 .paths() 133 .secrets() 134 .join("service_wrapping_key"); 135 fs::write(&credential_path, credential_bytes).expect("credential artifact"); 136 fs::set_permissions(&credential_path, fs::Permissions::from_mode(0o600)) 137 .expect("credential mode"); 138 let credential = 139 resolve_rhi_wrapping_credential(&runtime, &binding).expect("credential resolution"); 140 141 let material = RhiEncryptedIdentityProvisioningMaterial::new( 142 secret, 143 digest("radroots.rhi.wave-100-b.data-key.v1"), 144 [7; 24], 145 [9; 24], 146 ) 147 .expect("provisioning material"); 148 let provisioned = provision_rhi_encrypted_identity(&binding, &credential, material) 149 .expect("create-new identity provisioning"); 150 assert_eq!(provisioned.public_identity().as_hex(), expected_identity); 151 let opened = 152 open_rhi_encrypted_identity(&binding, &credential).expect("existing identity envelope"); 153 assert_eq!(opened.public_identity().as_hex(), expected_identity); 154 155 prepare_secure_directory(runtime.context().paths().state()); 156 let (applied_at, build) = migration_evidence(); 157 initialize_rhi_state(&runtime, &metadata, applied_at, &build) 158 .await 159 .expect("create-new state initialization"); 160 let state = open_rhi_state_read_write(&runtime, &metadata, applied_at, &build) 161 .await 162 .expect("existing state open"); 163 state.close().await.expect("explicit state close"); 164 165 let state_bytes = fs::read(runtime.artifacts().state_database()).expect("state database"); 166 for forbidden in [ 167 secret.as_slice(), 168 credential_bytes.as_slice(), 169 b"RRS1".as_slice(), 170 b"RHWK".as_slice(), 171 b"service_wrapping_key".as_slice(), 172 ] { 173 assert!( 174 !contains_bytes(&state_bytes, forbidden), 175 "state database contains protected identity material" 176 ); 177 } 178 assert_eq!( 179 runtime.identity_path().parent(), 180 Some(runtime.context().paths().secrets()) 181 ); 182 assert_ne!( 183 runtime.context().paths().state(), 184 runtime.context().paths().secrets() 185 ); 186 assert_eq!( 187 fs::metadata(runtime.identity_path()) 188 .expect("identity metadata") 189 .permissions() 190 .mode() 191 & 0o777, 192 0o600 193 ); 194 assert_eq!( 195 fs::metadata(&credential_path) 196 .expect("credential metadata") 197 .permissions() 198 .mode() 199 & 0o777, 200 0o600 201 ); 202 } 203 204 #[test] 205 fn wave_two_contracts_freeze_backup_exclusion_at_the_sealed_maintenance_boundary() { 206 let envelope: serde_json::Value = 207 serde_json::from_str(ENVELOPE_CONTRACT).expect("envelope contract"); 208 let credential: serde_json::Value = 209 serde_json::from_str(CREDENTIAL_CONTRACT).expect("credential contract"); 210 assert_eq!(envelope["backup"]["state_backup_includes_envelope"], false); 211 assert_eq!( 212 envelope["backup"]["state_backup_includes_wrapping_credential"], 213 false 214 ); 215 assert_eq!( 216 envelope["backup"]["state_backup_includes_plaintext_identity"], 217 false 218 ); 219 assert_eq!(credential["backup_included"], false); 220 assert!(STATE_HOST_SOURCE.contains("capture_online_backup")); 221 assert!(!STATE_HOST_SOURCE.contains("verify_backup_bundle")); 222 assert!(STATE_MAINTENANCE_SOURCE.contains("verify_backup_bundle")); 223 assert!(!STATE_HOST_SOURCE.contains("finalize_staged_restore")); 224 } 225 226 #[test] 227 fn wave_two_keeps_configuration_state_identity_and_credential_authorities_separate() { 228 for forbidden in [ 229 "resolve_rhi_wrapping_credential", 230 "RhiWrappingCredential", 231 "provision_rhi_encrypted_identity", 232 "open_rhi_encrypted_identity", 233 "service_wrapping_key", 234 ] { 235 assert!(!STATE_HOST_SOURCE.contains(forbidden)); 236 } 237 for forbidden in ["sqlx::", "std::fs::", "std::env::"] { 238 assert!( 239 !CONFIG_SOURCE.contains(forbidden), 240 "configuration parser contains forbidden authority {forbidden}" 241 ); 242 } 243 for source in [CREDENTIAL_SOURCE, ENVELOPE_SOURCE] { 244 for forbidden in ["sqlx::", "rusqlite::", "state.sqlite"] { 245 assert!(!source.contains(forbidden)); 246 } 247 } 248 let root = Path::new(env!("CARGO_MANIFEST_DIR")); 249 assert!(!root.join("src/host_identity.rs").exists()); 250 assert!(!root.join("src/identity_storage.rs").exists()); 251 }