myc

Self-custodial remote signer for Radroots apps
git clone https://radroots.dev/git/myc.git
Log | Files | Refs | README | LICENSE

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 }