lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

verify.rs (69825B)


      1 //! Untrusted backup bundle verification.
      2 
      3 use core::{fmt, num::NonZeroU64};
      4 use std::path::Path;
      5 
      6 use crate::{
      7     BackupManifestSha256, ServiceBackupManifest, ServiceDatabaseIdentity, ServiceDatabaseMetadata,
      8     ServiceSqliteError, ServiceSqliteErrorKind,
      9 };
     10 
     11 #[cfg(any(target_os = "linux", target_os = "macos"))]
     12 use {
     13     core::num::NonZeroU32,
     14     radroots_runtime_paths::{InstanceId, ServiceId},
     15     radroots_storage::event::SourceGeneration,
     16     rustix::{
     17         fs::{Dir, FileType, Mode, OFlags, fstat, open, openat},
     18         process::geteuid,
     19     },
     20     sha2::{Digest, Sha256},
     21     sqlx::{ConnectOptions, Connection as _, Row, SqliteConnection, sqlite::SqliteConnectOptions},
     22     std::{
     23         error::Error,
     24         fs::File,
     25         io::{Read, Seek, SeekFrom},
     26         os::fd::AsRawFd,
     27         os::unix::ffi::OsStrExt,
     28         path::PathBuf,
     29     },
     30 };
     31 
     32 #[cfg(any(target_os = "linux", target_os = "macos"))]
     33 const MAX_BUNDLE_PATH_BYTES: usize = 4_096;
     34 #[cfg(any(target_os = "linux", target_os = "macos"))]
     35 const HASH_BUFFER_BYTES: usize = 64 * 1_024;
     36 
     37 /// Non-forgeable proof that one retained backup member passed v1 verification.
     38 ///
     39 /// The proof is bound to retained directory and file descriptors, not only to
     40 /// their pathnames. It is not restore or replacement authority. Later restore
     41 /// work must copy from the retained member and reverify the staged copy.
     42 ///
     43 /// External callers cannot construct the proof or obtain its raw handles:
     44 ///
     45 /// ```compile_fail
     46 /// use radroots_service_sqlite::VerifiedServiceBackup;
     47 /// let _forged = VerifiedServiceBackup {};
     48 /// ```
     49 ///
     50 /// ```compile_fail
     51 /// # fn inspect(proof: &radroots_service_sqlite::VerifiedServiceBackup) {
     52 /// let _raw = proof.state_file();
     53 /// # }
     54 /// ```
     55 pub struct VerifiedServiceBackup {
     56     manifest: ServiceBackupManifest,
     57     database_metadata: ServiceDatabaseMetadata,
     58     #[cfg(any(target_os = "linux", target_os = "macos"))]
     59     binding: VerifiedBundleBinding,
     60 }
     61 
     62 impl VerifiedServiceBackup {
     63     /// Returns the exact canonical manifest bound by this proof.
     64     #[must_use]
     65     pub const fn manifest(&self) -> &ServiceBackupManifest {
     66         &self.manifest
     67     }
     68 
     69     /// Returns the actual validated application metadata read from the member.
     70     #[must_use]
     71     pub const fn database_metadata(&self) -> &ServiceDatabaseMetadata {
     72         &self.database_metadata
     73     }
     74 
     75     #[cfg(any(target_os = "linux", target_os = "macos"))]
     76     #[allow(dead_code)]
     77     pub(crate) fn state_file(&self) -> &File {
     78         &self.binding.state
     79     }
     80 
     81     #[cfg(any(target_os = "linux", target_os = "macos"))]
     82     #[allow(dead_code)]
     83     pub(crate) fn validate_binding(&self) -> Result<(), ServiceSqliteError> {
     84         self.binding.validate()
     85     }
     86 }
     87 
     88 impl fmt::Debug for VerifiedServiceBackup {
     89     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     90         formatter
     91             .debug_struct("VerifiedServiceBackup")
     92             .field("service", &"[redacted]")
     93             .field("instance", &"[redacted]")
     94             .field("manifest_digest", &"[redacted]")
     95             .field(
     96                 "state_schema_version",
     97                 &self.database_metadata.state_schema_version(),
     98             )
     99             .field("member", &"[retained]")
    100             .finish()
    101     }
    102 }
    103 
    104 /// Verifies untrusted canonical manifest bytes and their exact singleton bundle.
    105 ///
    106 /// The expected digest, expected database identity, and positive maximum member
    107 /// size are trusted caller inputs. This function is deliberately synchronous:
    108 /// callers own the supervised blocking worker and positive deadline. It never
    109 /// creates, copies, deletes, restores, or replaces filesystem state.
    110 pub fn verify_backup_bundle(
    111     manifest_bytes: &[u8],
    112     expected_manifest_digest: BackupManifestSha256,
    113     bundle_directory: &Path,
    114     expected_identity: &ServiceDatabaseIdentity,
    115     maximum_state_bytes: NonZeroU64,
    116 ) -> Result<VerifiedServiceBackup, ServiceSqliteError> {
    117     #[cfg(any(target_os = "linux", target_os = "macos"))]
    118     {
    119         verify_backup_bundle_native(
    120             manifest_bytes,
    121             expected_manifest_digest,
    122             bundle_directory,
    123             expected_identity,
    124             maximum_state_bytes,
    125         )
    126     }
    127     #[cfg(not(any(target_os = "linux", target_os = "macos")))]
    128     {
    129         let _ = (
    130             manifest_bytes,
    131             expected_manifest_digest,
    132             bundle_directory,
    133             expected_identity,
    134             maximum_state_bytes,
    135         );
    136         Err(ServiceSqliteError::new(ServiceSqliteErrorKind::Backup))
    137     }
    138 }
    139 
    140 #[cfg(any(target_os = "linux", target_os = "macos"))]
    141 fn verify_backup_bundle_native(
    142     manifest_bytes: &[u8],
    143     expected_manifest_digest: BackupManifestSha256,
    144     bundle_directory: &Path,
    145     expected_identity: &ServiceDatabaseIdentity,
    146     maximum_state_bytes: NonZeroU64,
    147 ) -> Result<VerifiedServiceBackup, ServiceSqliteError> {
    148     require_verification_condition(
    149         manifest_bytes.len() <= crate::BACKUP_MANIFEST_CANONICAL_MAX_BYTES,
    150         VerificationFailureKind::Manifest,
    151     )?;
    152     let actual_manifest_digest: [u8; 32] = Sha256::digest(manifest_bytes).into();
    153     require_verification_condition(
    154         &actual_manifest_digest == expected_manifest_digest.as_bytes(),
    155         VerificationFailureKind::ManifestDigest,
    156     )?;
    157     let manifest = ServiceBackupManifest::from_canonical_bytes(manifest_bytes)
    158         .map_err(|source| verification_source(VerificationFailureKind::Manifest, source))?;
    159     verify_manifest_intent(&manifest, expected_identity)?;
    160 
    161     let member = manifest
    162         .members()
    163         .first()
    164         .ok_or_else(|| verification_error(VerificationFailureKind::Inventory))?;
    165     require_verification_condition(
    166         member.byte_length() <= i64::MAX as u64
    167             && member.byte_length() <= maximum_state_bytes.get(),
    168         VerificationFailureKind::MemberLength,
    169     )?;
    170 
    171     let binding = VerifiedBundleBinding::open(bundle_directory, member.byte_length())?;
    172     binding.validate_inventory()?;
    173     let first_digest = binding.hash_state(maximum_state_bytes)?;
    174     require_verification_condition(
    175         &first_digest == member.sha256().as_bytes(),
    176         VerificationFailureKind::MemberDigest,
    177     )?;
    178     binding.validate()?;
    179 
    180     let database_metadata = futures::executor::block_on(async {
    181         let mut connection = open_sqlite_from_retained_state(&binding).await?;
    182         apply_connection_policy(&mut connection).await?;
    183         binding.validate()?;
    184         verify_database_inventory(&mut connection).await?;
    185         binding.validate()?;
    186         let database_metadata =
    187             verify_database_metadata(&mut connection, &manifest, expected_identity).await?;
    188         binding.validate()?;
    189         verify_integrity(&mut connection).await?;
    190         binding.validate()?;
    191         connection.close().await.map_err(integrity_source)?;
    192         Ok::<_, ServiceSqliteError>(database_metadata)
    193     })?;
    194 
    195     binding.validate_inventory()?;
    196     let final_digest = binding.hash_state(maximum_state_bytes)?;
    197     require_backup_digests(&first_digest, &final_digest, member.sha256().as_bytes())?;
    198     binding.validate()?;
    199 
    200     Ok(VerifiedServiceBackup {
    201         manifest,
    202         database_metadata,
    203         binding,
    204     })
    205 }
    206 
    207 #[cfg(any(target_os = "linux", target_os = "macos"))]
    208 fn require_backup_digests(
    209     first: &[u8; 32],
    210     final_digest: &[u8; 32],
    211     expected: &[u8; 32],
    212 ) -> Result<(), ServiceSqliteError> {
    213     require_verification_condition(
    214         crate::all_constraints([
    215             first == expected,
    216             final_digest == expected,
    217             first == final_digest,
    218         ]),
    219         VerificationFailureKind::MemberDigest,
    220     )
    221 }
    222 
    223 #[cfg(any(target_os = "linux", target_os = "macos"))]
    224 fn verify_manifest_intent(
    225     manifest: &ServiceBackupManifest,
    226     expected: &ServiceDatabaseIdentity,
    227 ) -> Result<(), ServiceSqliteError> {
    228     require_verification_condition(
    229         crate::all_constraints([
    230             manifest.service() == expected.service(),
    231             manifest.instance() == expected.instance(),
    232             manifest.source_generation() == expected.source_generation(),
    233             manifest.state_schema_version() <= expected.supported_state_schema_version(),
    234         ]),
    235         VerificationFailureKind::Intent,
    236     )?;
    237     Ok(())
    238 }
    239 
    240 #[cfg(any(target_os = "linux", target_os = "macos"))]
    241 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    242 struct FileIdentity {
    243     device: u64,
    244     inode: u64,
    245 }
    246 
    247 #[cfg(any(target_os = "linux", target_os = "macos"))]
    248 struct VerifiedBundleBinding {
    249     path: PathBuf,
    250     directory: File,
    251     directory_identity: FileIdentity,
    252     state: File,
    253     state_identity: FileIdentity,
    254     state_length: u64,
    255 }
    256 
    257 #[cfg(any(target_os = "linux", target_os = "macos"))]
    258 impl VerifiedBundleBinding {
    259     fn open(path: &Path, expected_length: u64) -> Result<Self, ServiceSqliteError> {
    260         validate_bundle_path(path)?;
    261         let directory = File::from(
    262             open(
    263                 path,
    264                 OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
    265                 Mode::empty(),
    266             )
    267             .map_err(|source| {
    268                 verification_source(VerificationFailureKind::BundleDirectory, source)
    269             })?,
    270         );
    271         let directory_identity = validate_directory(&directory)?;
    272         let state = File::from(
    273             openat(
    274                 &directory,
    275                 crate::BACKUP_STATE_MEMBER_NAME,
    276                 OFlags::RDONLY | OFlags::NONBLOCK | OFlags::NOFOLLOW | OFlags::CLOEXEC,
    277                 Mode::empty(),
    278             )
    279             .map_err(|source| verification_source(VerificationFailureKind::Inventory, source))?,
    280         );
    281         let (state_identity, state_length) = validate_state(&state)?;
    282         require_verification_condition(
    283             state_length == expected_length,
    284             VerificationFailureKind::MemberLength,
    285         )?;
    286         let binding = Self {
    287             path: path.to_path_buf(),
    288             directory,
    289             directory_identity,
    290             state,
    291             state_identity,
    292             state_length,
    293         };
    294         binding.validate()?;
    295         Ok(binding)
    296     }
    297 
    298     fn validate(&self) -> Result<(), ServiceSqliteError> {
    299         let current_directory = File::from(
    300             open(
    301                 &self.path,
    302                 OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
    303                 Mode::empty(),
    304             )
    305             .map_err(|source| {
    306                 verification_source(VerificationFailureKind::BindingChanged, source)
    307             })?,
    308         );
    309         require_verification_condition(
    310             validate_directory(&self.directory)? == self.directory_identity,
    311             VerificationFailureKind::BindingChanged,
    312         )?;
    313         require_verification_condition(
    314             validate_directory(&current_directory)? == self.directory_identity,
    315             VerificationFailureKind::BindingChanged,
    316         )?;
    317         let current_state = File::from(
    318             openat(
    319                 &self.directory,
    320                 crate::BACKUP_STATE_MEMBER_NAME,
    321                 OFlags::RDONLY | OFlags::NONBLOCK | OFlags::NOFOLLOW | OFlags::CLOEXEC,
    322                 Mode::empty(),
    323             )
    324             .map_err(|source| {
    325                 verification_source(VerificationFailureKind::BindingChanged, source)
    326             })?,
    327         );
    328         for state in [&self.state, &current_state] {
    329             let (identity, length) = validate_state(state)?;
    330             require_verification_condition(
    331                 (identity, length) == (self.state_identity, self.state_length),
    332                 VerificationFailureKind::BindingChanged,
    333             )?;
    334         }
    335         Ok(())
    336     }
    337 
    338     fn validate_inventory(&self) -> Result<(), ServiceSqliteError> {
    339         self.validate()?;
    340         let mut directory = Dir::read_from(&self.directory)
    341             .map_err(|source| verification_source(VerificationFailureKind::Inventory, source))?;
    342         let mut seen_state = false;
    343         let mut meaningful = 0_u8;
    344         while let Some(entry) = directory.read() {
    345             let entry = entry.map_err(|source| {
    346                 verification_source(VerificationFailureKind::Inventory, source)
    347             })?;
    348             let name = entry.file_name().to_bytes();
    349             if name == b"." || name == b".." {
    350                 continue;
    351             }
    352             meaningful = meaningful
    353                 .checked_add(1)
    354                 .ok_or_else(|| verification_error(VerificationFailureKind::Inventory))?;
    355             require_verification_condition(
    356                 crate::all_constraints([
    357                     meaningful <= 1,
    358                     name == crate::BACKUP_STATE_MEMBER_NAME.as_bytes(),
    359                 ]),
    360                 VerificationFailureKind::Inventory,
    361             )?;
    362             seen_state = true;
    363         }
    364         require_verification_condition(seen_state, VerificationFailureKind::Inventory)?;
    365         self.validate()
    366     }
    367 
    368     fn hash_state(&self, maximum: NonZeroU64) -> Result<[u8; 32], ServiceSqliteError> {
    369         self.validate()?;
    370         let mut state = self
    371             .state
    372             .try_clone()
    373             .map_err(|source| verification_source(VerificationFailureKind::MemberDigest, source))?;
    374         state
    375             .seek(SeekFrom::Start(0))
    376             .map_err(|source| verification_source(VerificationFailureKind::MemberDigest, source))?;
    377         let mut hasher = Sha256::new();
    378         let mut length = 0_u64;
    379         let mut buffer = [0_u8; HASH_BUFFER_BYTES];
    380         loop {
    381             let count = state.read(&mut buffer).map_err(|source| {
    382                 verification_source(VerificationFailureKind::MemberDigest, source)
    383             })?;
    384             if count == 0 {
    385                 break;
    386             }
    387             length = length
    388                 .checked_add(
    389                     u64::try_from(count)
    390                         .map_err(|_| verification_error(VerificationFailureKind::MemberLength))?,
    391                 )
    392                 .ok_or_else(|| verification_error(VerificationFailureKind::MemberLength))?;
    393             require_verification_condition(
    394                 crate::all_constraints([length <= i64::MAX as u64, length <= maximum.get()]),
    395                 VerificationFailureKind::MemberLength,
    396             )?;
    397             hasher.update(&buffer[..count]);
    398         }
    399         require_verification_condition(
    400             crate::all_constraints([length != 0, length == self.state_length]),
    401             VerificationFailureKind::MemberLength,
    402         )?;
    403         self.validate()?;
    404         Ok(hasher.finalize().into())
    405     }
    406 }
    407 
    408 #[cfg(any(target_os = "linux", target_os = "macos"))]
    409 fn validate_bundle_path(path: &Path) -> Result<(), ServiceSqliteError> {
    410     require_verification_condition(
    411         crate::all_constraints([
    412             path.is_absolute(),
    413             !path.as_os_str().as_bytes().is_empty(),
    414             path.as_os_str().as_bytes().len() <= MAX_BUNDLE_PATH_BYTES,
    415             !path.components().any(|part| {
    416                 matches!(
    417                     part,
    418                     std::path::Component::CurDir | std::path::Component::ParentDir
    419                 )
    420             }),
    421         ]),
    422         VerificationFailureKind::BundleDirectory,
    423     )?;
    424     Ok(())
    425 }
    426 
    427 #[cfg(any(target_os = "linux", target_os = "macos"))]
    428 fn validate_directory(directory: &File) -> Result<FileIdentity, ServiceSqliteError> {
    429     let status = fstat(directory)
    430         .map_err(|source| verification_source(VerificationFailureKind::BundleDirectory, source))?;
    431     let mode = crate::native_metadata::mode(status.st_mode) & 0o777;
    432     require_verification_condition(
    433         crate::native_metadata::restrictive_directory(
    434             FileType::from_raw_mode(status.st_mode).is_dir(),
    435             status.st_uid,
    436             geteuid().as_raw(),
    437             mode,
    438         ),
    439         VerificationFailureKind::Permissions,
    440     )?;
    441     file_identity(&status)
    442 }
    443 
    444 #[cfg(any(target_os = "linux", target_os = "macos"))]
    445 fn validate_state(file: &File) -> Result<(FileIdentity, u64), ServiceSqliteError> {
    446     let status = fstat(file)
    447         .map_err(|source| verification_source(VerificationFailureKind::Inventory, source))?;
    448     let mode = crate::native_metadata::mode(status.st_mode) & 0o777;
    449     let length = u64::try_from(status.st_size)
    450         .map_err(|_| verification_error(VerificationFailureKind::MemberLength))?;
    451     require_verification_condition(
    452         crate::native_metadata::restrictive_regular_file(
    453             FileType::from_raw_mode(status.st_mode).is_file(),
    454             crate::native_metadata::link_count(status.st_nlink),
    455             status.st_uid,
    456             geteuid().as_raw(),
    457             mode,
    458         ),
    459         VerificationFailureKind::Permissions,
    460     )?;
    461     require_verification_condition(
    462         crate::native_metadata::valid_artifact_length(length, None),
    463         VerificationFailureKind::MemberLength,
    464     )?;
    465     Ok((file_identity(&status)?, length))
    466 }
    467 
    468 #[cfg(any(target_os = "linux", target_os = "macos"))]
    469 fn file_identity(status: &rustix::fs::Stat) -> Result<FileIdentity, ServiceSqliteError> {
    470     Ok(FileIdentity {
    471         device: crate::native_metadata::device(status.st_dev)
    472             .map_err(|_| verification_error(VerificationFailureKind::BindingChanged))?,
    473         inode: status.st_ino,
    474     })
    475 }
    476 
    477 #[cfg(any(target_os = "linux", target_os = "macos"))]
    478 async fn open_sqlite_from_retained_state(
    479     binding: &VerifiedBundleBinding,
    480 ) -> Result<SqliteConnection, ServiceSqliteError> {
    481     let descriptor = binding.state.as_raw_fd();
    482     #[cfg(target_os = "linux")]
    483     let descriptor_path = format!("/proc/self/fd/{descriptor}");
    484     #[cfg(target_os = "macos")]
    485     let descriptor_path = format!("/dev/fd/{descriptor}");
    486     let options = SqliteConnectOptions::new()
    487         .filename(descriptor_path)
    488         .read_only(true)
    489         .immutable(true)
    490         .create_if_missing(false)
    491         .foreign_keys(false)
    492         .disable_statement_logging();
    493     SqliteConnection::connect_with(&options)
    494         .await
    495         .map_err(|source| verification_source(VerificationFailureKind::Inventory, source))
    496 }
    497 
    498 #[cfg(any(target_os = "linux", target_os = "macos"))]
    499 async fn apply_connection_policy(
    500     connection: &mut SqliteConnection,
    501 ) -> Result<(), ServiceSqliteError> {
    502     sqlx::query("PRAGMA query_only = ON")
    503         .execute(&mut *connection)
    504         .await
    505         .map_err(integrity_source)?;
    506     sqlx::query("PRAGMA trusted_schema = OFF")
    507         .execute(&mut *connection)
    508         .await
    509         .map_err(integrity_source)?;
    510     verify_connection_policy(connection).await
    511 }
    512 
    513 #[cfg(any(target_os = "linux", target_os = "macos"))]
    514 async fn verify_connection_policy(
    515     connection: &mut SqliteConnection,
    516 ) -> Result<(), ServiceSqliteError> {
    517     let query_only = sqlx::query_scalar::<_, i64>("PRAGMA query_only")
    518         .fetch_one(&mut *connection)
    519         .await
    520         .map_err(integrity_source)?;
    521     let trusted_schema = sqlx::query_scalar::<_, i64>("PRAGMA trusted_schema")
    522         .fetch_one(connection)
    523         .await
    524         .map_err(integrity_source)?;
    525     require_verification_connection_policy(query_only, trusted_schema)
    526 }
    527 
    528 #[cfg(any(target_os = "linux", target_os = "macos"))]
    529 fn require_verification_connection_policy(
    530     query_only: i64,
    531     trusted_schema: i64,
    532 ) -> Result<(), ServiceSqliteError> {
    533     crate::all_constraints([query_only == 1, trusted_schema == 0])
    534         .then_some(())
    535         .ok_or_else(|| integrity_error(IntegrityFailureKind::Policy))
    536 }
    537 
    538 #[cfg(any(target_os = "linux", target_os = "macos"))]
    539 async fn verify_database_inventory(
    540     connection: &mut SqliteConnection,
    541 ) -> Result<(), ServiceSqliteError> {
    542     let rows = sqlx::query(
    543         "SELECT
    544             seq,
    545             typeof(name) = 'text' AS name_type_ok,
    546             length(CAST(name AS BLOB)) AS name_length,
    547             substr(CAST(name AS BLOB), 1, 5) AS name_prefix
    548          FROM pragma_database_list
    549          LIMIT 2",
    550     )
    551     .fetch_all(connection)
    552     .await
    553     .map_err(integrity_source)?;
    554     let Some(first) = rows.first() else {
    555         return Err(integrity_error(IntegrityFailureKind::DatabaseInventory));
    556     };
    557     let sequence_matches = first.try_get::<i64, _>(0).ok() == Some(0);
    558     let name_matches = crate::persisted_value::bounded_utf8(
    559         first,
    560         "name_type_ok",
    561         "name_length",
    562         "name_prefix",
    563         1,
    564         4,
    565     ) == Some("main");
    566     require_verification_database_inventory(sequence_matches, name_matches, rows.len() > 1)
    567 }
    568 
    569 #[cfg(any(target_os = "linux", target_os = "macos"))]
    570 fn require_verification_database_inventory(
    571     sequence_matches: bool,
    572     name_matches: bool,
    573     has_extra: bool,
    574 ) -> Result<(), ServiceSqliteError> {
    575     crate::all_constraints([sequence_matches, name_matches, !has_extra])
    576         .then_some(())
    577         .ok_or_else(|| integrity_error(IntegrityFailureKind::DatabaseInventory))
    578 }
    579 
    580 #[cfg(any(target_os = "linux", target_os = "macos"))]
    581 async fn verify_database_metadata(
    582     connection: &mut SqliteConnection,
    583     manifest: &ServiceBackupManifest,
    584     expected: &ServiceDatabaseIdentity,
    585 ) -> Result<ServiceDatabaseMetadata, ServiceSqliteError> {
    586     let object_rows = sqlx::query(
    587         "SELECT type
    588              FROM main.sqlite_schema
    589              WHERE name = 'radroots_service_metadata'
    590              LIMIT 2",
    591     )
    592     .fetch_all(&mut *connection)
    593     .await
    594     .map_err(metadata_source)?;
    595     let object = object_rows.first().ok_or_else(metadata_error)?;
    596     crate::require_condition(
    597         object.try_get::<&str, _>(0).ok() == Some("table") && object_rows.len() == 1,
    598         ServiceSqliteErrorKind::Metadata,
    599     )?;
    600 
    601     let application_id = sqlx::query_scalar::<_, i64>("PRAGMA application_id")
    602         .fetch_one(&mut *connection)
    603         .await
    604         .map_err(metadata_source)?;
    605     let application_id = u32::try_from(application_id)
    606         .ok()
    607         .and_then(|value| crate::ServiceSqliteApplicationId::new(value).ok())
    608         .ok_or_else(metadata_error)?;
    609 
    610     let rows = sqlx::query(
    611         "SELECT
    612                 CASE WHEN typeof(singleton) = 'integer' THEN singleton END,
    613                 typeof(service_id) = 'text' AS service_id_type_ok,
    614                 length(CAST(service_id AS BLOB)) AS service_id_length,
    615                 substr(CAST(service_id AS BLOB), 1, 129) AS service_id_prefix,
    616                 typeof(instance_id) = 'text' AS instance_id_type_ok,
    617                 length(CAST(instance_id AS BLOB)) AS instance_id_length,
    618                 substr(CAST(instance_id AS BLOB), 1, 129) AS instance_id_prefix,
    619                 typeof(source_generation) = 'blob' AS source_generation_type_ok,
    620                 length(source_generation) AS source_generation_length,
    621                 substr(source_generation, 1, 33) AS source_generation_prefix,
    622                 CASE WHEN typeof(state_schema_version) = 'integer'
    623                      THEN state_schema_version END,
    624                 CASE WHEN typeof(created_at_unix_ms) = 'integer'
    625                      THEN created_at_unix_ms END
    626              FROM radroots_service_metadata
    627              LIMIT 2",
    628     )
    629     .fetch_all(connection)
    630     .await
    631     .map_err(metadata_source)?;
    632     let row = rows.first().ok_or_else(metadata_error)?;
    633     let singleton = row.try_get::<Option<i64>, _>(0).map_err(metadata_source)?;
    634     crate::require_condition(rows.len() == 1, ServiceSqliteErrorKind::Metadata)?;
    635     if singleton != Some(1) {
    636         return Err(metadata_error());
    637     }
    638     let service = crate::persisted_value::bounded_utf8(
    639         row,
    640         "service_id_type_ok",
    641         "service_id_length",
    642         "service_id_prefix",
    643         1,
    644         crate::persisted_value::MAX_IDENTIFIER_UTF8_BYTES,
    645     )
    646     .and_then(|value| ServiceId::new(value).ok())
    647     .ok_or_else(metadata_error)?;
    648     let instance = crate::persisted_value::bounded_utf8(
    649         row,
    650         "instance_id_type_ok",
    651         "instance_id_length",
    652         "instance_id_prefix",
    653         1,
    654         crate::persisted_value::MAX_IDENTIFIER_UTF8_BYTES,
    655     )
    656     .and_then(|value| InstanceId::new(value).ok())
    657     .ok_or_else(metadata_error)?;
    658     let generation = crate::persisted_value::bounded_bytes(
    659         row,
    660         "source_generation_type_ok",
    661         "source_generation_length",
    662         "source_generation_prefix",
    663         32,
    664         32,
    665     )
    666     .and_then(|value| <[u8; 32]>::try_from(value).ok())
    667     .and_then(|value| SourceGeneration::new(value).ok())
    668     .ok_or_else(metadata_error)?;
    669     let schema = row.try_get::<Option<i64>, _>(10).map_err(metadata_source)?;
    670     let created_at = row.try_get::<Option<i64>, _>(11).map_err(metadata_source)?;
    671     let (Some(schema), Some(created_at)) = (schema, created_at) else {
    672         return Err(metadata_error());
    673     };
    674     let schema = NonZeroU32::new(u32::try_from(schema).map_err(|_| metadata_error())?)
    675         .ok_or_else(metadata_error)?;
    676     let created_at = u64::try_from(created_at).map_err(|_| metadata_error())?;
    677 
    678     require_verification_metadata_projection([
    679         service == *expected.service(),
    680         instance == *expected.instance(),
    681         generation == expected.source_generation(),
    682         application_id == expected.application_id(),
    683         service == *manifest.service(),
    684         instance == *manifest.instance(),
    685         generation == manifest.source_generation(),
    686         schema == manifest.state_schema_version(),
    687         schema <= expected.supported_state_schema_version(),
    688     ])?;
    689     ServiceDatabaseMetadata::from_verified_backup(
    690         service,
    691         instance,
    692         generation,
    693         schema,
    694         created_at,
    695         application_id,
    696     )
    697     .map_err(|_| metadata_error())
    698 }
    699 
    700 #[cfg(any(target_os = "linux", target_os = "macos"))]
    701 async fn verify_integrity(connection: &mut SqliteConnection) -> Result<(), ServiceSqliteError> {
    702     let rows = sqlx::query(crate::persisted_value::INTEGRITY_CHECK_SQL)
    703         .fetch_all(&mut *connection)
    704         .await
    705         .map_err(integrity_source)?;
    706     let row = rows
    707         .first()
    708         .ok_or_else(|| integrity_error(IntegrityFailureKind::Sqlite))?;
    709     let value = crate::persisted_value::bounded_integrity_bytes(row);
    710     require_verification_integrity_projection(value == Some(b"ok"), rows.len() > 1)?;
    711     let violation = sqlx::query_scalar::<_, i64>("SELECT 1 FROM pragma_foreign_key_check LIMIT 1")
    712         .fetch_optional(connection)
    713         .await
    714         .map_err(integrity_source)?;
    715     require_integrity_condition(violation.is_none(), IntegrityFailureKind::ForeignKeys)?;
    716     Ok(())
    717 }
    718 
    719 #[cfg(any(target_os = "linux", target_os = "macos"))]
    720 fn require_verification_metadata_projection(matches: [bool; 9]) -> Result<(), ServiceSqliteError> {
    721     crate::require_condition(
    722         crate::all_constraints(matches),
    723         ServiceSqliteErrorKind::Metadata,
    724     )
    725 }
    726 
    727 #[cfg(any(target_os = "linux", target_os = "macos"))]
    728 fn require_verification_integrity_projection(
    729     value_matches: bool,
    730     has_extra: bool,
    731 ) -> Result<(), ServiceSqliteError> {
    732     crate::all_constraints([value_matches, !has_extra])
    733         .then_some(())
    734         .ok_or_else(|| integrity_error(IntegrityFailureKind::Sqlite))
    735 }
    736 
    737 #[cfg(any(target_os = "linux", target_os = "macos"))]
    738 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    739 enum VerificationFailureKind {
    740     Manifest,
    741     ManifestDigest,
    742     Intent,
    743     BundleDirectory,
    744     Inventory,
    745     Permissions,
    746     MemberLength,
    747     MemberDigest,
    748     BindingChanged,
    749 }
    750 
    751 #[cfg(any(target_os = "linux", target_os = "macos"))]
    752 struct VerificationFailure {
    753     kind: VerificationFailureKind,
    754     source: Option<Box<dyn Error + Send + Sync + 'static>>,
    755 }
    756 
    757 #[cfg(any(target_os = "linux", target_os = "macos"))]
    758 impl fmt::Debug for VerificationFailure {
    759     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    760         formatter
    761             .debug_struct("BackupVerificationFailure")
    762             .field("kind", &self.kind)
    763             .field("source", &self.source.as_ref().map(|_| "[redacted]"))
    764             .finish()
    765     }
    766 }
    767 
    768 #[cfg(any(target_os = "linux", target_os = "macos"))]
    769 impl fmt::Display for VerificationFailure {
    770     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    771         formatter.write_str(match self.kind {
    772             VerificationFailureKind::Manifest => "backup manifest is invalid",
    773             VerificationFailureKind::ManifestDigest => "backup manifest digest does not match",
    774             VerificationFailureKind::Intent => "backup intent does not match",
    775             VerificationFailureKind::BundleDirectory => "backup bundle directory is invalid",
    776             VerificationFailureKind::Inventory => "backup member inventory is invalid",
    777             VerificationFailureKind::Permissions => "backup permissions are invalid",
    778             VerificationFailureKind::MemberLength => "backup member length is invalid",
    779             VerificationFailureKind::MemberDigest => "backup member digest does not match",
    780             VerificationFailureKind::BindingChanged => "backup member binding changed",
    781         })
    782     }
    783 }
    784 
    785 #[cfg(any(target_os = "linux", target_os = "macos"))]
    786 impl Error for VerificationFailure {
    787     fn source(&self) -> Option<&(dyn Error + 'static)> {
    788         self.source
    789             .as_deref()
    790             .map(|source| source as &(dyn Error + 'static))
    791     }
    792 }
    793 
    794 #[cfg(any(target_os = "linux", target_os = "macos"))]
    795 fn verification_error(kind: VerificationFailureKind) -> ServiceSqliteError {
    796     ServiceSqliteError::with_source(
    797         ServiceSqliteErrorKind::Backup,
    798         VerificationFailure { kind, source: None },
    799     )
    800 }
    801 
    802 #[cfg(any(target_os = "linux", target_os = "macos"))]
    803 fn require_verification_condition(
    804     condition: bool,
    805     kind: VerificationFailureKind,
    806 ) -> Result<(), ServiceSqliteError> {
    807     if condition {
    808         Ok(())
    809     } else {
    810         Err(verification_error(kind))
    811     }
    812 }
    813 
    814 #[cfg(any(target_os = "linux", target_os = "macos"))]
    815 fn verification_source(
    816     kind: VerificationFailureKind,
    817     source: impl Error + Send + Sync + 'static,
    818 ) -> ServiceSqliteError {
    819     ServiceSqliteError::with_source(
    820         ServiceSqliteErrorKind::Backup,
    821         VerificationFailure {
    822             kind,
    823             source: Some(Box::new(source)),
    824         },
    825     )
    826 }
    827 
    828 #[cfg(any(target_os = "linux", target_os = "macos"))]
    829 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    830 enum IntegrityFailureKind {
    831     Policy,
    832     DatabaseInventory,
    833     Sqlite,
    834     ForeignKeys,
    835 }
    836 
    837 #[cfg(any(target_os = "linux", target_os = "macos"))]
    838 #[derive(Debug)]
    839 struct IntegrityFailure(IntegrityFailureKind);
    840 
    841 #[cfg(any(target_os = "linux", target_os = "macos"))]
    842 impl fmt::Display for IntegrityFailure {
    843     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    844         formatter.write_str(match self.0 {
    845             IntegrityFailureKind::Policy => "backup SQLite policy is invalid",
    846             IntegrityFailureKind::DatabaseInventory => "backup database inventory is invalid",
    847             IntegrityFailureKind::Sqlite => "backup SQLite integrity is invalid",
    848             IntegrityFailureKind::ForeignKeys => "backup foreign-key integrity is invalid",
    849         })
    850     }
    851 }
    852 
    853 #[cfg(any(target_os = "linux", target_os = "macos"))]
    854 impl Error for IntegrityFailure {}
    855 
    856 #[cfg(any(target_os = "linux", target_os = "macos"))]
    857 fn integrity_error(kind: IntegrityFailureKind) -> ServiceSqliteError {
    858     ServiceSqliteError::with_source(ServiceSqliteErrorKind::Integrity, IntegrityFailure(kind))
    859 }
    860 
    861 #[cfg(any(target_os = "linux", target_os = "macos"))]
    862 fn require_integrity_condition(
    863     condition: bool,
    864     kind: IntegrityFailureKind,
    865 ) -> Result<(), ServiceSqliteError> {
    866     if condition {
    867         Ok(())
    868     } else {
    869         Err(integrity_error(kind))
    870     }
    871 }
    872 
    873 #[cfg(any(target_os = "linux", target_os = "macos"))]
    874 fn integrity_source(source: sqlx::Error) -> ServiceSqliteError {
    875     ServiceSqliteError::with_source(ServiceSqliteErrorKind::Integrity, source)
    876 }
    877 
    878 #[cfg(any(target_os = "linux", target_os = "macos"))]
    879 fn metadata_error() -> ServiceSqliteError {
    880     ServiceSqliteError::new(ServiceSqliteErrorKind::Metadata)
    881 }
    882 
    883 #[cfg(any(target_os = "linux", target_os = "macos"))]
    884 fn metadata_source(source: sqlx::Error) -> ServiceSqliteError {
    885     ServiceSqliteError::with_source(ServiceSqliteErrorKind::Metadata, source)
    886 }
    887 
    888 #[cfg(test)]
    889 mod tests {
    890     #[cfg(any(target_os = "linux", target_os = "macos"))]
    891     use {
    892         super::*,
    893         crate::{ServiceSqliteApplicationId, ServiceSqlitePaths},
    894         radroots_runtime_paths::{
    895             RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPathResolver, RadrootsPlatform,
    896             RuntimeContext, RuntimeContextBootstrap, RuntimeContextSource,
    897         },
    898         std::{
    899             collections::BTreeSet, fs, io::Write, os::unix::fs::PermissionsExt, path::Path,
    900             process::Command,
    901         },
    902     };
    903 
    904     #[cfg(any(target_os = "linux", target_os = "macos"))]
    905     fn open_test_database(path: &Path) -> SqliteConnection {
    906         futures::executor::block_on(SqliteConnection::connect_with(
    907             &SqliteConnectOptions::new()
    908                 .filename(path)
    909                 .create_if_missing(true)
    910                 .foreign_keys(false)
    911                 .disable_statement_logging(),
    912         ))
    913         .expect("database")
    914     }
    915 
    916     #[cfg(any(target_os = "linux", target_os = "macos"))]
    917     #[test]
    918     fn verification_projection_helpers_reject_every_independent_drift() {
    919         let digest = [7_u8; 32];
    920         assert!(require_backup_digests(&digest, &digest, &digest).is_ok());
    921         for changed in 0..3 {
    922             let mut values = [digest; 3];
    923             values[changed][0] ^= 1;
    924             assert!(require_backup_digests(&values[0], &values[1], &values[2]).is_err());
    925         }
    926 
    927         assert!(require_verification_connection_policy(1, 0).is_ok());
    928         for values in [(0, 0), (1, 1), (0, 1)] {
    929             assert!(require_verification_connection_policy(values.0, values.1).is_err());
    930         }
    931 
    932         assert!(require_verification_database_inventory(true, true, false).is_ok());
    933         for (sequence, name, extra) in [
    934             (false, true, false),
    935             (true, false, false),
    936             (false, false, false),
    937             (true, true, true),
    938         ] {
    939             assert!(require_verification_database_inventory(sequence, name, extra).is_err());
    940         }
    941 
    942         assert!(require_verification_metadata_projection([true; 9]).is_ok());
    943         for changed in 0..9 {
    944             let mut matches = [true; 9];
    945             matches[changed] = false;
    946             assert!(require_verification_metadata_projection(matches).is_err());
    947         }
    948 
    949         assert!(require_verification_integrity_projection(true, false).is_ok());
    950         for (value_matches, extra) in [(false, false), (true, true)] {
    951             assert!(require_verification_integrity_projection(value_matches, extra).is_err());
    952         }
    953     }
    954 
    955     #[cfg(any(target_os = "linux", target_os = "macos"))]
    956     #[test]
    957     fn verification_and_integrity_failure_inventories_are_complete() {
    958         let verification_cases = [
    959             (
    960                 VerificationFailureKind::Manifest,
    961                 "backup manifest is invalid",
    962             ),
    963             (
    964                 VerificationFailureKind::ManifestDigest,
    965                 "backup manifest digest does not match",
    966             ),
    967             (
    968                 VerificationFailureKind::Intent,
    969                 "backup intent does not match",
    970             ),
    971             (
    972                 VerificationFailureKind::BundleDirectory,
    973                 "backup bundle directory is invalid",
    974             ),
    975             (
    976                 VerificationFailureKind::Inventory,
    977                 "backup member inventory is invalid",
    978             ),
    979             (
    980                 VerificationFailureKind::Permissions,
    981                 "backup permissions are invalid",
    982             ),
    983             (
    984                 VerificationFailureKind::MemberLength,
    985                 "backup member length is invalid",
    986             ),
    987             (
    988                 VerificationFailureKind::MemberDigest,
    989                 "backup member digest does not match",
    990             ),
    991             (
    992                 VerificationFailureKind::BindingChanged,
    993                 "backup member binding changed",
    994             ),
    995         ];
    996         for (kind, message) in verification_cases {
    997             let plain = VerificationFailure { kind, source: None };
    998             assert_eq!(plain.to_string(), message);
    999             assert!(plain.source().is_none());
   1000             let sourced = VerificationFailure {
   1001                 kind,
   1002                 source: Some(Box::new(std::io::Error::other("private-cause"))),
   1003             };
   1004             assert_eq!(sourced.to_string(), message);
   1005             assert!(sourced.source().is_some());
   1006             assert!(format!("{sourced:?}").contains("[redacted]"));
   1007             assert!(require_verification_condition(true, kind).is_ok());
   1008             assert_eq!(
   1009                 require_verification_condition(false, kind)
   1010                     .expect_err("false condition")
   1011                     .kind(),
   1012                 ServiceSqliteErrorKind::Backup
   1013             );
   1014         }
   1015 
   1016         for (kind, message) in [
   1017             (
   1018                 IntegrityFailureKind::Policy,
   1019                 "backup SQLite policy is invalid",
   1020             ),
   1021             (
   1022                 IntegrityFailureKind::DatabaseInventory,
   1023                 "backup database inventory is invalid",
   1024             ),
   1025             (
   1026                 IntegrityFailureKind::Sqlite,
   1027                 "backup SQLite integrity is invalid",
   1028             ),
   1029             (
   1030                 IntegrityFailureKind::ForeignKeys,
   1031                 "backup foreign-key integrity is invalid",
   1032             ),
   1033         ] {
   1034             let failure = IntegrityFailure(kind);
   1035             assert_eq!(failure.to_string(), message);
   1036             assert!(failure.source().is_none());
   1037             assert!(format!("{failure:?}").contains(&format!("{kind:?}")));
   1038             assert!(require_integrity_condition(true, kind).is_ok());
   1039             assert_eq!(
   1040                 require_integrity_condition(false, kind)
   1041                     .expect_err("false condition")
   1042                     .kind(),
   1043                 ServiceSqliteErrorKind::Integrity
   1044             );
   1045         }
   1046     }
   1047 
   1048     #[cfg(any(target_os = "linux", target_os = "macos"))]
   1049     struct Fixture {
   1050         _root: tempfile::TempDir,
   1051         bundle: PathBuf,
   1052         paths: ServiceSqlitePaths,
   1053         metadata: ServiceDatabaseMetadata,
   1054         identity: ServiceDatabaseIdentity,
   1055         manifest: ServiceBackupManifest,
   1056     }
   1057 
   1058     #[cfg(any(target_os = "linux", target_os = "macos"))]
   1059     impl Fixture {
   1060         fn new(bundle_name: &str) -> Self {
   1061             let root = tempfile::tempdir().expect("temporary root");
   1062             let paths = paths(root.path(), "myc", "primary");
   1063             let metadata = ServiceDatabaseMetadata::new(
   1064                 &paths,
   1065                 SourceGeneration::new([9; 32]).expect("source generation"),
   1066                 NonZeroU32::new(1).expect("schema"),
   1067                 1_700_000_000_000,
   1068                 ServiceSqliteApplicationId::new(0x5244_5351).expect("application ID"),
   1069             )
   1070             .expect("metadata");
   1071             let bundle = root.path().join(bundle_name);
   1072             fs::create_dir(&bundle).expect("bundle");
   1073             fs::set_permissions(&bundle, fs::Permissions::from_mode(0o700)).expect("bundle mode");
   1074             create_database(&bundle.join(crate::BACKUP_STATE_MEMBER_NAME), &metadata);
   1075             let manifest = manifest_for(&bundle, &metadata);
   1076             let identity = metadata.identity();
   1077             Self {
   1078                 _root: root,
   1079                 bundle,
   1080                 paths,
   1081                 metadata,
   1082                 identity,
   1083                 manifest,
   1084             }
   1085         }
   1086 
   1087         fn maximum(&self) -> NonZeroU64 {
   1088             NonZeroU64::new(self.manifest.members()[0].byte_length()).expect("member length")
   1089         }
   1090 
   1091         fn verify(&self) -> Result<VerifiedServiceBackup, ServiceSqliteError> {
   1092             verify_backup_bundle(
   1093                 self.manifest.canonical_bytes(),
   1094                 self.manifest.digest(),
   1095                 &self.bundle,
   1096                 &self.identity,
   1097                 self.maximum(),
   1098             )
   1099         }
   1100 
   1101         fn refresh_manifest(&mut self) {
   1102             self.manifest = manifest_for(&self.bundle, &self.metadata);
   1103         }
   1104     }
   1105 
   1106     #[cfg(any(target_os = "linux", target_os = "macos"))]
   1107     fn paths(root: &Path, service: &str, instance: &str) -> ServiceSqlitePaths {
   1108         let context = RuntimeContext::resolve(
   1109             &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()),
   1110             RuntimeContextBootstrap::new(
   1111                 RadrootsPathProfile::RepoLocal,
   1112                 Some(root.to_path_buf()),
   1113                 RuntimeContextSource::BootstrapCli,
   1114                 RuntimeContextSource::BootstrapCli,
   1115             )
   1116             .expect("bootstrap"),
   1117             ServiceId::new(service).expect("service"),
   1118             InstanceId::new(instance).expect("instance"),
   1119         )
   1120         .expect("context");
   1121         ServiceSqlitePaths::from_runtime_context(&context).expect("paths")
   1122     }
   1123 
   1124     #[cfg(any(target_os = "linux", target_os = "macos"))]
   1125     fn create_database(path: &Path, metadata: &ServiceDatabaseMetadata) {
   1126         let mut connection = open_test_database(path);
   1127         futures::executor::block_on(async {
   1128             sqlx::raw_sql(
   1129                 "CREATE TABLE radroots_service_metadata (
   1130                     singleton INTEGER PRIMARY KEY,
   1131                     service_id TEXT NOT NULL,
   1132                     instance_id TEXT NOT NULL,
   1133                     source_generation BLOB NOT NULL,
   1134                     state_schema_version INTEGER NOT NULL,
   1135                     created_at_unix_ms INTEGER NOT NULL
   1136                  );
   1137                  CREATE TABLE verify_probe (value INTEGER NOT NULL);
   1138                  INSERT INTO verify_probe (value) VALUES (41), (42);",
   1139             )
   1140             .execute(&mut connection)
   1141             .await
   1142             .expect("schema");
   1143             sqlx::query(
   1144                 "INSERT INTO radroots_service_metadata (
   1145                     singleton, service_id, instance_id, source_generation,
   1146                     state_schema_version, created_at_unix_ms
   1147                  ) VALUES (1, ?, ?, ?, ?, ?)",
   1148             )
   1149             .bind(metadata.service().as_str())
   1150             .bind(metadata.instance().as_str())
   1151             .bind(metadata.source_generation().as_bytes().as_slice())
   1152             .bind(i64::from(metadata.state_schema_version().get()))
   1153             .bind(i64::try_from(metadata.created_at_unix_ms()).expect("creation time"))
   1154             .execute(&mut connection)
   1155             .await
   1156             .expect("metadata row");
   1157             let application_id = format!(
   1158                 "PRAGMA application_id = {}",
   1159                 metadata.application_id().get()
   1160             );
   1161             sqlx::query(sqlx::AssertSqlSafe(application_id.as_str()))
   1162                 .execute(&mut connection)
   1163                 .await
   1164                 .expect("application ID");
   1165             connection.close().await.expect("close fixture");
   1166         });
   1167         fs::set_permissions(path, fs::Permissions::from_mode(0o600)).expect("state mode");
   1168     }
   1169 
   1170     #[cfg(any(target_os = "linux", target_os = "macos"))]
   1171     fn manifest_for(bundle: &Path, metadata: &ServiceDatabaseMetadata) -> ServiceBackupManifest {
   1172         let bytes = fs::read(bundle.join(crate::BACKUP_STATE_MEMBER_NAME)).expect("state bytes");
   1173         ServiceBackupManifest::from_capture(
   1174             metadata,
   1175             crate::BackupCreatedAtUnixMs::new(1_700_000_000_100).expect("capture time"),
   1176             u64::try_from(bytes.len()).expect("length"),
   1177             crate::BackupMemberSha256::from_bytes(Sha256::digest(&bytes).into()),
   1178         )
   1179         .expect("manifest")
   1180     }
   1181 
   1182     #[cfg(any(target_os = "linux", target_os = "macos"))]
   1183     fn digest(bytes: &[u8]) -> BackupManifestSha256 {
   1184         BackupManifestSha256::from_bytes(Sha256::digest(bytes).into())
   1185     }
   1186 
   1187     #[cfg(any(target_os = "linux", target_os = "macos"))]
   1188     fn inventory(path: &Path) -> BTreeSet<std::ffi::OsString> {
   1189         fs::read_dir(path)
   1190             .expect("inventory")
   1191             .map(|entry| entry.expect("entry").file_name())
   1192             .collect()
   1193     }
   1194 
   1195     #[cfg(any(target_os = "linux", target_os = "macos"))]
   1196     #[test]
   1197     fn verifies_exact_bundle_without_mutating_it_and_redacts_the_capability() {
   1198         let fixture = Fixture::new("verified bundle %?#");
   1199         let state = fixture.bundle.join(crate::BACKUP_STATE_MEMBER_NAME);
   1200         let before_bytes = fs::read(&state).expect("before bytes");
   1201         let before_modified = fs::metadata(&state)
   1202             .expect("before metadata")
   1203             .modified()
   1204             .expect("before mtime");
   1205         let before_modes = (
   1206             fs::metadata(&fixture.bundle)
   1207                 .expect("directory metadata")
   1208                 .permissions()
   1209                 .mode()
   1210                 & 0o777,
   1211             fs::metadata(&state)
   1212                 .expect("state metadata")
   1213                 .permissions()
   1214                 .mode()
   1215                 & 0o777,
   1216         );
   1217         let before_inventory = inventory(&fixture.bundle);
   1218 
   1219         let verified = fixture.verify().expect("verified backup");
   1220         assert_eq!(verified.manifest(), &fixture.manifest);
   1221         assert_eq!(verified.database_metadata(), &fixture.metadata);
   1222         let debug = format!("{verified:?}");
   1223         for forbidden in [
   1224             fixture.bundle.to_string_lossy().as_ref(),
   1225             "myc",
   1226             "primary",
   1227             "52445351",
   1228         ] {
   1229             assert!(!debug.contains(forbidden));
   1230         }
   1231         assert_eq!(fs::read(&state).expect("after bytes"), before_bytes);
   1232         assert_eq!(
   1233             fs::metadata(&state)
   1234                 .expect("after metadata")
   1235                 .modified()
   1236                 .expect("after mtime"),
   1237             before_modified
   1238         );
   1239         assert_eq!(
   1240             (
   1241                 fs::metadata(&fixture.bundle)
   1242                     .expect("directory metadata")
   1243                     .permissions()
   1244                     .mode()
   1245                     & 0o777,
   1246                 fs::metadata(&state)
   1247                     .expect("state metadata")
   1248                     .permissions()
   1249                     .mode()
   1250                     & 0o777,
   1251             ),
   1252             before_modes
   1253         );
   1254         assert_eq!(inventory(&fixture.bundle), before_inventory);
   1255     }
   1256 
   1257     #[cfg(any(target_os = "linux", target_os = "macos"))]
   1258     #[test]
   1259     fn digest_intent_schema_and_member_size_fail_closed() {
   1260         let fixture = Fixture::new("intent");
   1261         let wrong_digest = BackupManifestSha256::from_bytes([7; 32]);
   1262         assert_eq!(
   1263             verify_backup_bundle(
   1264                 fixture.manifest.canonical_bytes(),
   1265                 wrong_digest,
   1266                 &fixture.bundle,
   1267                 &fixture.identity,
   1268                 fixture.maximum(),
   1269             )
   1270             .expect_err("digest mismatch")
   1271             .kind(),
   1272             ServiceSqliteErrorKind::Backup
   1273         );
   1274 
   1275         let tampered_time = String::from_utf8(fixture.manifest.canonical_bytes().to_vec())
   1276             .expect("manifest text")
   1277             .replace("1700000000100", "1700000000101");
   1278         assert_eq!(
   1279             verify_backup_bundle(
   1280                 tampered_time.as_bytes(),
   1281                 fixture.manifest.digest(),
   1282                 &fixture.bundle,
   1283                 &fixture.identity,
   1284                 fixture.maximum(),
   1285             )
   1286             .expect_err("creation time tamper")
   1287             .kind(),
   1288             ServiceSqliteErrorKind::Backup
   1289         );
   1290 
   1291         let smaller = NonZeroU64::new(fixture.maximum().get() - 1).expect("smaller limit");
   1292         assert_eq!(
   1293             verify_backup_bundle(
   1294                 fixture.manifest.canonical_bytes(),
   1295                 fixture.manifest.digest(),
   1296                 &fixture.bundle,
   1297                 &fixture.identity,
   1298                 smaller,
   1299             )
   1300             .expect_err("member over caller limit")
   1301             .kind(),
   1302             ServiceSqliteErrorKind::Backup
   1303         );
   1304 
   1305         let wrong_generation = ServiceDatabaseIdentity::new(
   1306             &fixture.paths,
   1307             SourceGeneration::new([8; 32]).expect("generation"),
   1308             NonZeroU32::new(1).expect("schema"),
   1309             fixture.identity.application_id(),
   1310         );
   1311         assert_eq!(
   1312             verify_backup_bundle(
   1313                 fixture.manifest.canonical_bytes(),
   1314                 fixture.manifest.digest(),
   1315                 &fixture.bundle,
   1316                 &wrong_generation,
   1317                 fixture.maximum(),
   1318             )
   1319             .expect_err("generation intent")
   1320             .kind(),
   1321             ServiceSqliteErrorKind::Backup
   1322         );
   1323 
   1324         for (service, instance) in [("rhi", "primary"), ("myc", "secondary")] {
   1325             let wrong_paths = paths(fixture._root.path(), service, instance);
   1326             let wrong_identity = ServiceDatabaseIdentity::new(
   1327                 &wrong_paths,
   1328                 fixture.identity.source_generation(),
   1329                 fixture.identity.supported_state_schema_version(),
   1330                 fixture.identity.application_id(),
   1331             );
   1332             assert_eq!(
   1333                 verify_backup_bundle(
   1334                     fixture.manifest.canonical_bytes(),
   1335                     fixture.manifest.digest(),
   1336                     &fixture.bundle,
   1337                     &wrong_identity,
   1338                     fixture.maximum(),
   1339                 )
   1340                 .expect_err("service-instance intent")
   1341                 .kind(),
   1342                 ServiceSqliteErrorKind::Backup
   1343             );
   1344         }
   1345 
   1346         let wrong_application = ServiceDatabaseIdentity::new(
   1347             &fixture.paths,
   1348             fixture.identity.source_generation(),
   1349             NonZeroU32::new(1).expect("schema"),
   1350             ServiceSqliteApplicationId::new(7).expect("application"),
   1351         );
   1352         assert_eq!(
   1353             verify_backup_bundle(
   1354                 fixture.manifest.canonical_bytes(),
   1355                 fixture.manifest.digest(),
   1356                 &fixture.bundle,
   1357                 &wrong_application,
   1358                 fixture.maximum(),
   1359             )
   1360             .expect_err("application intent")
   1361             .kind(),
   1362             ServiceSqliteErrorKind::Metadata
   1363         );
   1364 
   1365         let schema_two = String::from_utf8(fixture.manifest.canonical_bytes().to_vec())
   1366             .expect("manifest text")
   1367             .replace("\"state_schema_version\":1", "\"state_schema_version\":2");
   1368         let supports_two = ServiceDatabaseIdentity::new(
   1369             &fixture.paths,
   1370             fixture.identity.source_generation(),
   1371             NonZeroU32::new(2).expect("schema"),
   1372             fixture.identity.application_id(),
   1373         );
   1374         assert_eq!(
   1375             verify_backup_bundle(
   1376                 schema_two.as_bytes(),
   1377                 digest(schema_two.as_bytes()),
   1378                 &fixture.bundle,
   1379                 &supports_two,
   1380                 fixture.maximum(),
   1381             )
   1382             .expect_err("manifest and database schema mismatch")
   1383             .kind(),
   1384             ServiceSqliteErrorKind::Metadata
   1385         );
   1386 
   1387         let too_large = String::from_utf8(fixture.manifest.canonical_bytes().to_vec())
   1388             .expect("manifest text")
   1389             .replace(
   1390                 &format!("\"byte_length\":{}", fixture.maximum().get()),
   1391                 "\"byte_length\":9223372036854775808",
   1392             );
   1393         assert_eq!(
   1394             verify_backup_bundle(
   1395                 too_large.as_bytes(),
   1396                 digest(too_large.as_bytes()),
   1397                 &fixture.bundle,
   1398                 &fixture.identity,
   1399                 NonZeroU64::new(u64::MAX).expect("maximum"),
   1400             )
   1401             .expect_err("signed representation overflow")
   1402             .kind(),
   1403             ServiceSqliteErrorKind::Backup
   1404         );
   1405     }
   1406 
   1407     #[cfg(any(target_os = "linux", target_os = "macos"))]
   1408     #[test]
   1409     fn restrictive_read_only_modes_are_allowed_and_broader_modes_reject() {
   1410         for (directory_mode, state_mode) in [
   1411             (0o700, 0o600),
   1412             (0o700, 0o400),
   1413             (0o500, 0o600),
   1414             (0o500, 0o400),
   1415         ] {
   1416             let fixture = Fixture::new("allowed-modes");
   1417             let state = fixture.bundle.join(crate::BACKUP_STATE_MEMBER_NAME);
   1418             fs::set_permissions(&state, fs::Permissions::from_mode(state_mode))
   1419                 .expect("state mode");
   1420             fs::set_permissions(&fixture.bundle, fs::Permissions::from_mode(directory_mode))
   1421                 .expect("directory mode");
   1422             fixture.verify().expect("restrictive mode verifies");
   1423         }
   1424         for directory_mode in [0o400, 0o710, 0o750] {
   1425             let fixture = Fixture::new("bad-directory-mode");
   1426             fs::set_permissions(&fixture.bundle, fs::Permissions::from_mode(directory_mode))
   1427                 .expect("directory mode");
   1428             assert_eq!(
   1429                 fixture.verify().expect_err("invalid directory mode").kind(),
   1430                 ServiceSqliteErrorKind::Backup
   1431             );
   1432         }
   1433         for state_mode in [0o000, 0o500, 0o640, 0o700] {
   1434             let fixture = Fixture::new("bad-state-mode");
   1435             fs::set_permissions(
   1436                 fixture.bundle.join(crate::BACKUP_STATE_MEMBER_NAME),
   1437                 fs::Permissions::from_mode(state_mode),
   1438             )
   1439             .expect("state mode");
   1440             assert_eq!(
   1441                 fixture.verify().expect_err("invalid state mode").kind(),
   1442                 ServiceSqliteErrorKind::Backup
   1443             );
   1444         }
   1445     }
   1446 
   1447     #[cfg(any(target_os = "linux", target_os = "macos"))]
   1448     #[test]
   1449     fn inventory_links_and_same_length_tampering_reject() {
   1450         let fixture = Fixture::new("extra");
   1451         fs::write(fixture.bundle.join("state.sqlite-wal"), b"foreign").expect("sidecar");
   1452         assert_eq!(
   1453             fixture.verify().expect_err("extra member").kind(),
   1454             ServiceSqliteErrorKind::Backup
   1455         );
   1456 
   1457         let fixture = Fixture::new("hardlink");
   1458         fs::hard_link(
   1459             fixture.bundle.join(crate::BACKUP_STATE_MEMBER_NAME),
   1460             fixture.bundle.join("other"),
   1461         )
   1462         .expect("hard link");
   1463         assert_eq!(
   1464             fixture.verify().expect_err("hard-linked member").kind(),
   1465             ServiceSqliteErrorKind::Backup
   1466         );
   1467 
   1468         let fixture = Fixture::new("missing");
   1469         fs::remove_file(fixture.bundle.join(crate::BACKUP_STATE_MEMBER_NAME))
   1470             .expect("remove state");
   1471         assert_eq!(
   1472             fixture.verify().expect_err("missing member").kind(),
   1473             ServiceSqliteErrorKind::Backup
   1474         );
   1475 
   1476         let fixture = Fixture::new("symlink");
   1477         let state = fixture.bundle.join(crate::BACKUP_STATE_MEMBER_NAME);
   1478         let held = fixture.bundle.join("held-state");
   1479         fs::rename(&state, &held).expect("move state");
   1480         std::os::unix::fs::symlink(&held, &state).expect("symlink state");
   1481         assert_eq!(
   1482             fixture.verify().expect_err("symlink member").kind(),
   1483             ServiceSqliteErrorKind::Backup
   1484         );
   1485 
   1486         let fixture = Fixture::new("directory-member");
   1487         let state = fixture.bundle.join(crate::BACKUP_STATE_MEMBER_NAME);
   1488         fs::remove_file(&state).expect("remove state");
   1489         fs::create_dir(&state).expect("directory member");
   1490         fs::set_permissions(&state, fs::Permissions::from_mode(0o700))
   1491             .expect("directory member mode");
   1492         assert_eq!(
   1493             fixture.verify().expect_err("directory member").kind(),
   1494             ServiceSqliteErrorKind::Backup
   1495         );
   1496 
   1497         let fixture = Fixture::new("fifo-member");
   1498         let state = fixture.bundle.join(crate::BACKUP_STATE_MEMBER_NAME);
   1499         fs::remove_file(&state).expect("remove state");
   1500         assert!(
   1501             Command::new("mkfifo")
   1502                 .arg(&state)
   1503                 .status()
   1504                 .expect("mkfifo")
   1505                 .success()
   1506         );
   1507         fs::set_permissions(&state, fs::Permissions::from_mode(0o600)).expect("fifo mode");
   1508         assert_eq!(
   1509             fixture.verify().expect_err("fifo member").kind(),
   1510             ServiceSqliteErrorKind::Backup
   1511         );
   1512 
   1513         let fixture = Fixture::new("tamper");
   1514         let state = fixture.bundle.join(crate::BACKUP_STATE_MEMBER_NAME);
   1515         let mut bytes = fs::read(&state).expect("state");
   1516         let index = bytes.len() - 1;
   1517         bytes[index] ^= 1;
   1518         fs::write(&state, bytes).expect("same-length tamper");
   1519         fs::set_permissions(&state, fs::Permissions::from_mode(0o600)).expect("state mode");
   1520         assert_eq!(
   1521             fixture.verify().expect_err("member digest mismatch").kind(),
   1522             ServiceSqliteErrorKind::Backup
   1523         );
   1524     }
   1525 
   1526     #[cfg(any(target_os = "linux", target_os = "macos"))]
   1527     #[test]
   1528     fn bounded_metadata_and_foreign_key_integrity_reject() {
   1529         for statement in [
   1530             "UPDATE radroots_service_metadata SET singleton = 2",
   1531             "UPDATE radroots_service_metadata SET state_schema_version = 'invalid'",
   1532             "UPDATE radroots_service_metadata SET created_at_unix_ms = 'invalid'",
   1533         ] {
   1534             let mut fixture = Fixture::new("malformed-metadata");
   1535             fixture.verify().expect("valid original bundle");
   1536             let state = fixture.bundle.join(crate::BACKUP_STATE_MEMBER_NAME);
   1537             let mut connection = open_test_database(&state);
   1538             futures::executor::block_on(async {
   1539                 sqlx::query(sqlx::AssertSqlSafe(statement))
   1540                     .execute(&mut connection)
   1541                     .await
   1542                     .expect("substituted metadata value");
   1543                 connection.close().await.expect("close fixture");
   1544             });
   1545             fixture.refresh_manifest();
   1546             assert_eq!(
   1547                 fixture.verify().expect_err("invalid metadata").kind(),
   1548                 ServiceSqliteErrorKind::Metadata
   1549             );
   1550         }
   1551         let mut metadata_fixture = Fixture::new("oversized-metadata");
   1552         let state = metadata_fixture
   1553             .bundle
   1554             .join(crate::BACKUP_STATE_MEMBER_NAME);
   1555         let mut connection = open_test_database(&state);
   1556         futures::executor::block_on(async {
   1557             sqlx::query("UPDATE radroots_service_metadata SET service_id = ?")
   1558                 .bind("x".repeat(129))
   1559                 .execute(&mut connection)
   1560                 .await
   1561                 .expect("oversized metadata");
   1562             connection.close().await.expect("close");
   1563         });
   1564         metadata_fixture.refresh_manifest();
   1565         assert_eq!(
   1566             metadata_fixture
   1567                 .verify()
   1568                 .expect_err("oversized metadata")
   1569                 .kind(),
   1570             ServiceSqliteErrorKind::Metadata
   1571         );
   1572 
   1573         let mut view_fixture = Fixture::new("metadata-view");
   1574         let state = view_fixture.bundle.join(crate::BACKUP_STATE_MEMBER_NAME);
   1575         let mut connection = open_test_database(&state);
   1576         let generation = "09".repeat(32);
   1577         futures::executor::block_on(async {
   1578             let statement = format!(
   1579                 "DROP TABLE radroots_service_metadata;
   1580                  CREATE VIEW radroots_service_metadata AS
   1581                  SELECT
   1582                      1 AS singleton,
   1583                      'myc' AS service_id,
   1584                      'primary' AS instance_id,
   1585                      X'{generation}' AS source_generation,
   1586                      1 AS state_schema_version,
   1587                      1700000000000 AS created_at_unix_ms;"
   1588             );
   1589             sqlx::raw_sql(sqlx::AssertSqlSafe(statement.as_str()))
   1590                 .execute(&mut connection)
   1591                 .await
   1592                 .expect("metadata view");
   1593             connection.close().await.expect("close");
   1594         });
   1595         view_fixture.refresh_manifest();
   1596         assert_eq!(
   1597             view_fixture.verify().expect_err("metadata view").kind(),
   1598             ServiceSqliteErrorKind::Metadata
   1599         );
   1600 
   1601         let mut foreign_key_fixture = Fixture::new("foreign-key");
   1602         let state = foreign_key_fixture
   1603             .bundle
   1604             .join(crate::BACKUP_STATE_MEMBER_NAME);
   1605         let mut connection = open_test_database(&state);
   1606         futures::executor::block_on(async {
   1607             sqlx::raw_sql(
   1608                 "PRAGMA foreign_keys = OFF;
   1609                  CREATE TABLE parent (id INTEGER PRIMARY KEY);
   1610                  CREATE TABLE child (
   1611                      id INTEGER PRIMARY KEY,
   1612                      parent_id INTEGER NOT NULL REFERENCES parent(id)
   1613                  );
   1614                  INSERT INTO child (id, parent_id) VALUES (1, 99);",
   1615             )
   1616             .execute(&mut connection)
   1617             .await
   1618             .expect("foreign-key violation");
   1619             connection.close().await.expect("close");
   1620         });
   1621         foreign_key_fixture.refresh_manifest();
   1622         assert_eq!(
   1623             foreign_key_fixture
   1624                 .verify()
   1625                 .expect_err("foreign-key violation")
   1626                 .kind(),
   1627             ServiceSqliteErrorKind::Integrity
   1628         );
   1629 
   1630         let mut corrupt_fixture = Fixture::new("corrupt-sqlite");
   1631         let state = corrupt_fixture.bundle.join(crate::BACKUP_STATE_MEMBER_NAME);
   1632         let mut connection = open_test_database(&state);
   1633         let (page_size, root_page) = futures::executor::block_on(async {
   1634             let page_size = sqlx::query_scalar::<_, i64>("PRAGMA page_size")
   1635                 .fetch_one(&mut connection)
   1636                 .await
   1637                 .expect("page size");
   1638             let root_page = sqlx::query_scalar::<_, i64>(
   1639                 "SELECT rootpage FROM sqlite_schema WHERE name = 'verify_probe'",
   1640             )
   1641             .fetch_one(&mut connection)
   1642             .await
   1643             .expect("probe root page");
   1644             connection.close().await.expect("close");
   1645             (page_size, root_page)
   1646         });
   1647         let page_size = u64::try_from(page_size).expect("positive page size");
   1648         let root_page = u64::try_from(root_page).expect("positive root page");
   1649         let corrupt_offset = root_page
   1650             .checked_sub(1)
   1651             .and_then(|page| page.checked_mul(page_size))
   1652             .expect("corrupt offset");
   1653         let mut state_file = fs::OpenOptions::new()
   1654             .write(true)
   1655             .open(&state)
   1656             .expect("open corrupt state");
   1657         state_file
   1658             .seek(SeekFrom::Start(corrupt_offset))
   1659             .expect("seek corrupt page");
   1660         state_file.write_all(&[0xff]).expect("corrupt page type");
   1661         state_file.sync_all().expect("sync corruption");
   1662         drop(state_file);
   1663         corrupt_fixture.refresh_manifest();
   1664         assert_eq!(
   1665             corrupt_fixture
   1666                 .verify()
   1667                 .expect_err("corrupt SQLite with trusted hashes")
   1668                 .kind(),
   1669             ServiceSqliteErrorKind::Integrity
   1670         );
   1671     }
   1672 
   1673     #[cfg(any(target_os = "linux", target_os = "macos"))]
   1674     #[test]
   1675     fn sqlite_connection_policy_drift_fails_closed() {
   1676         futures::executor::block_on(async {
   1677             let mut connection = SqliteConnection::connect("sqlite::memory:")
   1678                 .await
   1679                 .expect("memory database");
   1680             apply_connection_policy(&mut connection)
   1681                 .await
   1682                 .expect("governed policy");
   1683             verify_connection_policy(&mut connection)
   1684                 .await
   1685                 .expect("policy readback");
   1686 
   1687             sqlx::query("PRAGMA trusted_schema = ON")
   1688                 .execute(&mut connection)
   1689                 .await
   1690                 .expect("drift trusted schema");
   1691             assert_eq!(
   1692                 verify_connection_policy(&mut connection)
   1693                     .await
   1694                     .expect_err("trusted-schema drift")
   1695                     .kind(),
   1696                 ServiceSqliteErrorKind::Integrity
   1697             );
   1698 
   1699             sqlx::query("PRAGMA trusted_schema = OFF")
   1700                 .execute(&mut connection)
   1701                 .await
   1702                 .expect("restore trusted schema");
   1703             sqlx::query("PRAGMA query_only = OFF")
   1704                 .execute(&mut connection)
   1705                 .await
   1706                 .expect("drift query-only");
   1707             assert_eq!(
   1708                 verify_connection_policy(&mut connection)
   1709                     .await
   1710                     .expect_err("query-only drift")
   1711                     .kind(),
   1712                 ServiceSqliteErrorKind::Integrity
   1713             );
   1714         });
   1715     }
   1716 
   1717     #[cfg(any(target_os = "linux", target_os = "macos"))]
   1718     #[test]
   1719     fn attached_database_and_replaced_bindings_fail_closed() {
   1720         futures::executor::block_on(async {
   1721             let mut connection = SqliteConnection::connect("sqlite::memory:")
   1722                 .await
   1723                 .expect("memory database");
   1724             sqlx::query("ATTACH DATABASE ':memory:' AS extra")
   1725                 .execute(&mut connection)
   1726                 .await
   1727                 .expect("attach");
   1728             assert_eq!(
   1729                 verify_database_inventory(&mut connection)
   1730                     .await
   1731                     .expect_err("extra attachment")
   1732                     .kind(),
   1733                 ServiceSqliteErrorKind::Integrity
   1734             );
   1735         });
   1736 
   1737         let fixture = Fixture::new("replace-directory");
   1738         let binding = VerifiedBundleBinding::open(
   1739             &fixture.bundle,
   1740             fixture.manifest.members()[0].byte_length(),
   1741         )
   1742         .expect("binding");
   1743         let moved = fixture.bundle.with_extension("held");
   1744         fs::rename(&fixture.bundle, &moved).expect("move held directory");
   1745         fs::create_dir(&fixture.bundle).expect("replacement directory");
   1746         fs::set_permissions(&fixture.bundle, fs::Permissions::from_mode(0o700))
   1747             .expect("replacement mode");
   1748         fs::write(
   1749             fixture.bundle.join(crate::BACKUP_STATE_MEMBER_NAME),
   1750             b"replacement",
   1751         )
   1752         .expect("replacement state");
   1753         fs::set_permissions(
   1754             fixture.bundle.join(crate::BACKUP_STATE_MEMBER_NAME),
   1755             fs::Permissions::from_mode(0o600),
   1756         )
   1757         .expect("replacement state mode");
   1758 
   1759         futures::executor::block_on(async {
   1760             let mut retained_connection = open_sqlite_from_retained_state(&binding)
   1761                 .await
   1762                 .expect("open retained member");
   1763             apply_connection_policy(&mut retained_connection)
   1764                 .await
   1765                 .expect("retained connection policy");
   1766             verify_database_inventory(&mut retained_connection)
   1767                 .await
   1768                 .expect("retained database inventory");
   1769             assert_eq!(
   1770                 verify_database_metadata(
   1771                     &mut retained_connection,
   1772                     &fixture.manifest,
   1773                     &fixture.identity,
   1774                 )
   1775                 .await
   1776                 .expect("retained metadata"),
   1777                 fixture.metadata
   1778             );
   1779             verify_integrity(&mut retained_connection)
   1780                 .await
   1781                 .expect("retained integrity");
   1782             retained_connection
   1783                 .close()
   1784                 .await
   1785                 .expect("close retained member");
   1786         });
   1787 
   1788         assert_eq!(
   1789             binding
   1790                 .validate()
   1791                 .expect_err("directory replacement")
   1792                 .kind(),
   1793             ServiceSqliteErrorKind::Backup
   1794         );
   1795         let retained = fs::read(moved.join(crate::BACKUP_STATE_MEMBER_NAME))
   1796             .expect("retained member through moved directory");
   1797         let replacement = fs::read(fixture.bundle.join(crate::BACKUP_STATE_MEMBER_NAME))
   1798             .expect("replacement member");
   1799         assert_ne!(retained, replacement);
   1800         assert_eq!(replacement, b"replacement");
   1801 
   1802         let fixture = Fixture::new("replace-member");
   1803         let binding = VerifiedBundleBinding::open(
   1804             &fixture.bundle,
   1805             fixture.manifest.members()[0].byte_length(),
   1806         )
   1807         .expect("binding");
   1808         let state = fixture.bundle.join(crate::BACKUP_STATE_MEMBER_NAME);
   1809         let moved = fixture.bundle.join("held-state");
   1810         fs::rename(&state, &moved).expect("move held member");
   1811         fs::copy(&moved, &state).expect("replacement member");
   1812         fs::set_permissions(&state, fs::Permissions::from_mode(0o600)).expect("replacement mode");
   1813         assert_eq!(
   1814             binding.validate().expect_err("member replacement").kind(),
   1815             ServiceSqliteErrorKind::Backup
   1816         );
   1817         assert_eq!(
   1818             fs::read(&moved).expect("held member"),
   1819             fs::read(&state).expect("replacement")
   1820         );
   1821 
   1822         fs::remove_file(&state).expect("remove replacement");
   1823         assert!(
   1824             Command::new("mkfifo")
   1825                 .arg(&state)
   1826                 .status()
   1827                 .expect("mkfifo")
   1828                 .success()
   1829         );
   1830         fs::set_permissions(&state, fs::Permissions::from_mode(0o600)).expect("fifo mode");
   1831         assert_eq!(
   1832             binding.validate().expect_err("replacement fifo").kind(),
   1833             ServiceSqliteErrorKind::Backup
   1834         );
   1835     }
   1836 
   1837     #[cfg(any(target_os = "linux", target_os = "macos"))]
   1838     #[test]
   1839     fn public_diagnostics_do_not_render_untrusted_paths_or_contents() {
   1840         let fixture = Fixture::new("secret-path-value");
   1841         fs::write(fixture.bundle.join("secret-extra"), b"secret-content").expect("extra");
   1842         let error = fixture.verify().expect_err("invalid inventory");
   1843         assert_eq!(error.kind(), ServiceSqliteErrorKind::Backup);
   1844         for rendered in [error.to_string(), format!("{error:?}")] {
   1845             assert!(!rendered.contains("secret-path-value"));
   1846             assert!(!rendered.contains("secret-extra"));
   1847             assert!(!rendered.contains("secret-content"));
   1848         }
   1849     }
   1850 }