state_maintenance.rs (10909B)
1 //! Myc-bound integrity, backup, and offline restore integration. 2 3 use core::{fmt, num::NonZeroU64}; 4 use std::{error::Error, path::Path}; 5 6 use radroots_service_sqlite::{ 7 BackupManifestSha256, ServiceBackupManifest, ServiceDatabaseMetadata, ServiceSqliteError, 8 ServiceSqliteErrorKind, StagedServiceRestore, VerifiedServiceBackup, finalize_staged_restore, 9 stage_verified_restore, verify_backup_bundle, 10 }; 11 12 use crate::{MycRuntimeContext, MycStateMetadata, state_host}; 13 14 /// Stable source-free class for a Myc state-maintenance failure. 15 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 16 pub enum MycStateMaintenanceErrorKind { 17 InvalidEvidence, 18 InvalidMode, 19 Catalog, 20 Authority, 21 Open, 22 Metadata, 23 Migration, 24 Backup, 25 Restore, 26 Integrity, 27 Recovery, 28 } 29 30 impl MycStateMaintenanceErrorKind { 31 /// Returns the stable machine-readable failure code. 32 #[must_use] 33 pub const fn code(self) -> &'static str { 34 match self { 35 Self::InvalidEvidence => "state_maintenance_evidence_invalid", 36 Self::InvalidMode => "state_maintenance_mode_invalid", 37 Self::Catalog => "state_maintenance_catalog_invalid", 38 Self::Authority => "state_maintenance_authority_failed", 39 Self::Open => "state_maintenance_open_failed", 40 Self::Metadata => "state_maintenance_metadata_invalid", 41 Self::Migration => "state_maintenance_migration_invalid", 42 Self::Backup => "state_backup_failed", 43 Self::Restore => "state_restore_failed", 44 Self::Integrity => "state_integrity_failed", 45 Self::Recovery => "state_recovery_failed", 46 } 47 } 48 } 49 50 /// Redacted Myc state-maintenance failure. 51 #[derive(Clone, Copy, PartialEq, Eq)] 52 pub struct MycStateMaintenanceError { 53 kind: MycStateMaintenanceErrorKind, 54 } 55 56 impl MycStateMaintenanceError { 57 pub(crate) const fn new(kind: MycStateMaintenanceErrorKind) -> Self { 58 Self { kind } 59 } 60 61 pub(crate) fn from_sqlite(error: ServiceSqliteError) -> Self { 62 let kind = match error.kind() { 63 ServiceSqliteErrorKind::Authority => MycStateMaintenanceErrorKind::Authority, 64 ServiceSqliteErrorKind::Open 65 | ServiceSqliteErrorKind::Create 66 | ServiceSqliteErrorKind::Pragma => MycStateMaintenanceErrorKind::Open, 67 ServiceSqliteErrorKind::Metadata => MycStateMaintenanceErrorKind::Metadata, 68 ServiceSqliteErrorKind::Migration => MycStateMaintenanceErrorKind::Migration, 69 ServiceSqliteErrorKind::Backup => MycStateMaintenanceErrorKind::Backup, 70 ServiceSqliteErrorKind::Restore => MycStateMaintenanceErrorKind::Restore, 71 ServiceSqliteErrorKind::Integrity => MycStateMaintenanceErrorKind::Integrity, 72 ServiceSqliteErrorKind::Recovery => MycStateMaintenanceErrorKind::Recovery, 73 }; 74 Self::new(kind) 75 } 76 77 /// Returns the stable failure class. 78 #[must_use] 79 pub const fn kind(self) -> MycStateMaintenanceErrorKind { 80 self.kind 81 } 82 83 /// Returns the stable machine-readable failure code. 84 #[must_use] 85 pub const fn code(self) -> &'static str { 86 self.kind.code() 87 } 88 } 89 90 impl fmt::Display for MycStateMaintenanceError { 91 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 92 formatter.write_str(match self.kind { 93 MycStateMaintenanceErrorKind::InvalidEvidence => { 94 "Myc state maintenance evidence is invalid" 95 } 96 MycStateMaintenanceErrorKind::InvalidMode => { 97 "Myc state maintenance is unavailable in this host mode" 98 } 99 MycStateMaintenanceErrorKind::Catalog => "Myc state catalogs are invalid", 100 MycStateMaintenanceErrorKind::Authority => { 101 "Myc state maintenance authority could not be established" 102 } 103 MycStateMaintenanceErrorKind::Open => "Myc state maintenance could not open state", 104 MycStateMaintenanceErrorKind::Metadata => "Myc state metadata is invalid", 105 MycStateMaintenanceErrorKind::Migration => "Myc state migration history is invalid", 106 MycStateMaintenanceErrorKind::Backup => "Myc state backup failed", 107 MycStateMaintenanceErrorKind::Restore => "Myc state restore failed", 108 MycStateMaintenanceErrorKind::Integrity => "Myc state integrity check failed", 109 MycStateMaintenanceErrorKind::Recovery => "Myc state recovery failed", 110 }) 111 } 112 } 113 114 impl fmt::Debug for MycStateMaintenanceError { 115 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 116 formatter 117 .debug_struct("MycStateMaintenanceError") 118 .field("kind", &self.kind) 119 .finish() 120 } 121 } 122 123 impl Error for MycStateMaintenanceError {} 124 125 /// Retained exact-inode proof of one verified Myc backup. 126 /// 127 /// Construction is sealed to [`verify_myc_state_backup`]. No raw descriptor or 128 /// pathname is exposed. 129 /// 130 /// ```compile_fail 131 /// use myc::MycVerifiedStateBackup; 132 /// let _ = MycVerifiedStateBackup { inner: todo!() }; 133 /// ``` 134 pub struct MycVerifiedStateBackup { 135 inner: VerifiedServiceBackup, 136 } 137 138 impl MycVerifiedStateBackup { 139 /// Returns the admitted canonical manifest. 140 #[must_use] 141 pub const fn manifest(&self) -> &ServiceBackupManifest { 142 self.inner.manifest() 143 } 144 145 /// Returns the actual immutable database metadata read from the retained member. 146 #[must_use] 147 pub const fn database_metadata(&self) -> &ServiceDatabaseMetadata { 148 self.inner.database_metadata() 149 } 150 } 151 152 impl fmt::Debug for MycVerifiedStateBackup { 153 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 154 formatter.write_str("MycVerifiedStateBackup([redacted])") 155 } 156 } 157 158 /// Offline staged Myc replacement that retains exclusive writer authority. 159 /// 160 /// Construction is sealed to [`stage_myc_state_restore`]. Dropping this value 161 /// preserves the shared exact-inode cleanup and fail-closed evidence contract. 162 /// 163 /// ```compile_fail 164 /// use myc::MycStagedStateRestore; 165 /// let _ = MycStagedStateRestore { inner: todo!() }; 166 /// ``` 167 pub struct MycStagedStateRestore { 168 inner: StagedServiceRestore, 169 } 170 171 impl fmt::Debug for MycStagedStateRestore { 172 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 173 formatter.write_str("MycStagedStateRestore([redacted])") 174 } 175 } 176 177 /// Verifies an untrusted backup bundle against one sealed Myc state identity. 178 pub fn verify_myc_state_backup( 179 manifest_bytes: &[u8], 180 expected_manifest_digest: BackupManifestSha256, 181 bundle_directory: &Path, 182 expected: &MycStateMetadata, 183 maximum_state_bytes: NonZeroU64, 184 ) -> Result<MycVerifiedStateBackup, MycStateMaintenanceError> { 185 verify_backup_bundle( 186 manifest_bytes, 187 expected_manifest_digest, 188 bundle_directory, 189 &expected.database_identity(), 190 maximum_state_bytes, 191 ) 192 .map(|inner| MycVerifiedStateBackup { inner }) 193 .map_err(MycStateMaintenanceError::from_sqlite) 194 } 195 196 /// Copies and fully reverifies a verified backup beside closed Myc state. 197 /// 198 /// This operation acquires exclusive writer authority. It never creates a 199 /// recovery marker or replaces the live database. 200 pub async fn stage_myc_state_restore( 201 runtime: &MycRuntimeContext, 202 expected: &MycStateMetadata, 203 verified: MycVerifiedStateBackup, 204 ) -> Result<MycStagedStateRestore, MycStateMaintenanceError> { 205 state_host::require_metadata(runtime, expected).map_err(|_| { 206 MycStateMaintenanceError::new(MycStateMaintenanceErrorKind::InvalidEvidence) 207 })?; 208 let paths = state_host::state_paths(runtime).map_err(|_| { 209 MycStateMaintenanceError::new(MycStateMaintenanceErrorKind::InvalidEvidence) 210 })?; 211 let (migrations, schema) = state_host::catalogs() 212 .map_err(|_| MycStateMaintenanceError::new(MycStateMaintenanceErrorKind::Catalog))?; 213 stage_verified_restore( 214 &paths, 215 &expected.database_identity(), 216 &migrations, 217 &schema, 218 verified.inner, 219 ) 220 .await 221 .map(|inner| MycStagedStateRestore { inner }) 222 .map_err(MycStateMaintenanceError::from_sqlite) 223 } 224 225 /// Atomically installs a completely verified staged Myc restore. 226 /// 227 /// Success intentionally returns no open host. The next writable open owns 228 /// exact recovery evidence reconciliation before SQLite is exposed again. 229 pub async fn finalize_myc_state_restore( 230 staged: MycStagedStateRestore, 231 ) -> Result<(), MycStateMaintenanceError> { 232 finalize_staged_restore(staged.inner) 233 .await 234 .map_err(MycStateMaintenanceError::from_sqlite) 235 } 236 237 #[cfg(test)] 238 mod tests { 239 use super::*; 240 241 #[test] 242 fn shared_failures_map_to_the_closed_source_free_myc_vocabulary() { 243 for (source, expected) in [ 244 ( 245 ServiceSqliteErrorKind::Authority, 246 MycStateMaintenanceErrorKind::Authority, 247 ), 248 ( 249 ServiceSqliteErrorKind::Open, 250 MycStateMaintenanceErrorKind::Open, 251 ), 252 ( 253 ServiceSqliteErrorKind::Create, 254 MycStateMaintenanceErrorKind::Open, 255 ), 256 ( 257 ServiceSqliteErrorKind::Pragma, 258 MycStateMaintenanceErrorKind::Open, 259 ), 260 ( 261 ServiceSqliteErrorKind::Metadata, 262 MycStateMaintenanceErrorKind::Metadata, 263 ), 264 ( 265 ServiceSqliteErrorKind::Migration, 266 MycStateMaintenanceErrorKind::Migration, 267 ), 268 ( 269 ServiceSqliteErrorKind::Backup, 270 MycStateMaintenanceErrorKind::Backup, 271 ), 272 ( 273 ServiceSqliteErrorKind::Restore, 274 MycStateMaintenanceErrorKind::Restore, 275 ), 276 ( 277 ServiceSqliteErrorKind::Integrity, 278 MycStateMaintenanceErrorKind::Integrity, 279 ), 280 ( 281 ServiceSqliteErrorKind::Recovery, 282 MycStateMaintenanceErrorKind::Recovery, 283 ), 284 ] { 285 let mapped = MycStateMaintenanceError::from_sqlite(ServiceSqliteError::with_source( 286 source, 287 SensitiveSource, 288 )); 289 assert_eq!(mapped.kind(), expected); 290 assert!(Error::source(&mapped).is_none()); 291 let rendered = format!("{mapped} {mapped:?}"); 292 assert!(!rendered.contains("sensitive")); 293 assert!(!mapped.code().is_empty()); 294 } 295 } 296 297 #[derive(Debug)] 298 struct SensitiveSource; 299 300 impl fmt::Display for SensitiveSource { 301 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 302 formatter.write_str("sensitive /tmp/state.sqlite") 303 } 304 } 305 306 impl Error for SensitiveSource {} 307 }