myc

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

services_hardening_state_host.rs (9418B)


      1 #![forbid(unsafe_code)]
      2 #![cfg(any(target_os = "linux", target_os = "macos"))]
      3 
      4 use std::{error::Error, fs, os::unix::fs::PermissionsExt, path::Path};
      5 
      6 use myc::{
      7     MycConfigProfile, MycStateHostErrorKind, MycStateHostMode, MycStateMetadata,
      8     RadrootsHostEnvironment, RadrootsPathResolver, RadrootsPlatform, initialize_myc_state,
      9     open_myc_state_inspection, open_myc_state_read_write, parse_myc_cli_v1_from,
     10     parse_myc_config_v1, resolve_myc_runtime_context,
     11 };
     12 use radroots_service_sqlite::{MigrationAppliedAtUnixSeconds, MigrationBuildIdentity};
     13 use radroots_storage::event::SourceGeneration;
     14 
     15 const HOST_SOURCE: &str = include_str!("../src/state_host.rs");
     16 const LIB_SOURCE: &str = include_str!("../src/lib.rs");
     17 const CONFIG_EXAMPLE: &[u8] =
     18     include_bytes!("../contracts/services_hardening/config.v1.example.toml");
     19 
     20 fn runtime(root: &Path, instance: &str) -> myc::MycRuntimeContext {
     21     let root = root.to_str().expect("UTF-8 temporary root");
     22     let invocation = parse_myc_cli_v1_from([
     23         "myc",
     24         "--profile",
     25         "repo-local",
     26         "--instance",
     27         instance,
     28         "--repo-local-root",
     29         root,
     30         "run",
     31     ])
     32     .expect("valid test invocation");
     33     resolve_myc_runtime_context(
     34         &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()),
     35         &invocation,
     36     )
     37     .expect("runtime context")
     38 }
     39 
     40 fn prepare_state_directory(runtime: &myc::MycRuntimeContext) {
     41     let directory = runtime.context().paths().state();
     42     fs::create_dir_all(directory).expect("state directory");
     43     fs::set_permissions(directory, fs::Permissions::from_mode(0o700)).expect("state mode");
     44 }
     45 
     46 fn prepare_state_root(runtime: &myc::MycRuntimeContext) {
     47     let root = runtime
     48         .context()
     49         .paths()
     50         .state()
     51         .ancestors()
     52         .nth(3)
     53         .expect("state root");
     54     fs::create_dir_all(root).expect("state root");
     55     fs::set_permissions(root, fs::Permissions::from_mode(0o700)).expect("state root mode");
     56 }
     57 
     58 fn metadata(runtime: &myc::MycRuntimeContext) -> MycStateMetadata {
     59     let configuration =
     60         parse_myc_config_v1(CONFIG_EXAMPLE, MycConfigProfile::RepoLocal).expect("configuration");
     61     MycStateMetadata::new(
     62         runtime,
     63         &configuration,
     64         SourceGeneration::new([0x5a; 32]).expect("generation"),
     65         1_725_000_000_000,
     66     )
     67     .expect("metadata")
     68 }
     69 
     70 fn migration_evidence() -> (MigrationAppliedAtUnixSeconds, MigrationBuildIdentity) {
     71     let applied_at = MigrationAppliedAtUnixSeconds::new(1_725_000_000).expect("migration time");
     72     let build = MigrationBuildIdentity::new(
     73         env!("CARGO_PKG_VERSION"),
     74         "1111111111111111111111111111111111111111",
     75         "053d0c750bf9cd683c6ea37cefe7e79617ba629f",
     76         "rustc-test",
     77         "test-target",
     78         "service-host",
     79         1,
     80         myc::MYC_STATE_SCHEMA_VERSION,
     81         1,
     82         1,
     83         1,
     84     )
     85     .expect("build identity");
     86     (applied_at, build)
     87 }
     88 
     89 fn mismatched_migration_build() -> MigrationBuildIdentity {
     90     MigrationBuildIdentity::new(
     91         env!("CARGO_PKG_VERSION"),
     92         "1111111111111111111111111111111111111111",
     93         "053d0c750bf9cd683c6ea37cefe7e79617ba629f",
     94         "rustc-test",
     95         "test-target",
     96         "service-host",
     97         1,
     98         1,
     99         1,
    100         1,
    101         1,
    102     )
    103     .expect("structurally valid mismatched build")
    104 }
    105 
    106 #[tokio::test]
    107 async fn initialize_is_create_new_and_both_existing_open_modes_close_explicitly() {
    108     let directory = tempfile::tempdir().expect("temporary root");
    109     let runtime = runtime(directory.path(), "primary");
    110     prepare_state_root(&runtime);
    111     let metadata = metadata(&runtime);
    112     let state = runtime.artifacts().state_database();
    113     let lock = runtime.artifacts().state_lock();
    114 
    115     assert!(!runtime.context().paths().state().exists());
    116     assert!(!state.exists());
    117     let (applied_at, build) = migration_evidence();
    118     initialize_myc_state(&runtime, &metadata, applied_at, &build)
    119         .await
    120         .expect("create-new initialization");
    121     assert!(state.is_file());
    122     assert!(lock.is_file());
    123     assert_eq!(
    124         fs::metadata(state).unwrap().permissions().mode() & 0o777,
    125         0o600
    126     );
    127     assert_eq!(
    128         fs::metadata(lock).unwrap().permissions().mode() & 0o777,
    129         0o600
    130     );
    131 
    132     let duplicate = initialize_myc_state(&runtime, &metadata, applied_at, &build)
    133         .await
    134         .expect_err("second initialization must fail");
    135     assert_eq!(duplicate.kind(), MycStateHostErrorKind::Initialize);
    136 
    137     let writer = open_myc_state_read_write(&runtime, &metadata, applied_at, &build)
    138         .await
    139         .expect("existing writable state");
    140     assert_eq!(writer.mode(), MycStateHostMode::ReadWriteExisting);
    141     assert_eq!(
    142         format!("{writer:?}"),
    143         "MycStateHost { mode: ReadWriteExisting, state: \"[sealed]\" }"
    144     );
    145 
    146     let contended = open_myc_state_inspection(&runtime, &metadata)
    147         .await
    148         .expect_err("inspection must not bypass active writer authority");
    149     assert_eq!(contended.kind(), MycStateHostErrorKind::InspectionOpen);
    150     writer.close().await.expect("writer close");
    151     writer.close().await.expect("idempotent writer close");
    152 
    153     let inspection = open_myc_state_inspection(&runtime, &metadata)
    154         .await
    155         .expect("existing inspection state");
    156     assert_eq!(inspection.mode(), MycStateHostMode::ReadOnlyInspection);
    157     inspection.close().await.expect("inspection close");
    158 
    159     let writer = open_myc_state_read_write(&runtime, &metadata, applied_at, &build)
    160         .await
    161         .expect("authority reacquisition after explicit close");
    162     writer.close().await.expect("reopened writer close");
    163 }
    164 
    165 #[tokio::test]
    166 async fn missing_state_and_mismatched_evidence_fail_before_database_creation() {
    167     let invalid_directory = tempfile::tempdir().expect("invalid temporary root");
    168     let invalid_runtime = runtime(invalid_directory.path(), "invalid");
    169     let invalid_metadata = metadata(&invalid_runtime);
    170     let (applied_at, build) = migration_evidence();
    171     let invalid_build = initialize_myc_state(
    172         &invalid_runtime,
    173         &invalid_metadata,
    174         applied_at,
    175         &mismatched_migration_build(),
    176     )
    177     .await
    178     .expect_err("migration build contract mismatch");
    179     assert_eq!(invalid_build.kind(), MycStateHostErrorKind::InvalidEvidence);
    180     assert!(!invalid_runtime.context().paths().state().exists());
    181 
    182     let missing_directory = tempfile::tempdir().expect("missing temporary root");
    183     let missing_runtime = runtime(missing_directory.path(), "missing");
    184     prepare_state_root(&missing_runtime);
    185     let missing_metadata = metadata(&missing_runtime);
    186     let missing =
    187         open_myc_state_read_write(&missing_runtime, &missing_metadata, applied_at, &build)
    188             .await
    189             .expect_err("existing-only open never provisions the service suffix");
    190     assert_eq!(missing.kind(), MycStateHostErrorKind::ReadWriteOpen);
    191     assert!(!missing_runtime.context().paths().state().exists());
    192 
    193     let directory = tempfile::tempdir().expect("temporary root");
    194     let primary = runtime(directory.path(), "primary");
    195     let secondary = runtime(directory.path(), "secondary");
    196     prepare_state_directory(&primary);
    197     let primary_metadata = metadata(&primary);
    198 
    199     let missing = open_myc_state_read_write(&primary, &primary_metadata, applied_at, &build)
    200         .await
    201         .expect_err("missing state is never created by open");
    202     assert_eq!(missing.kind(), MycStateHostErrorKind::ReadWriteOpen);
    203     assert!(!primary.artifacts().state_database().exists());
    204 
    205     let mismatch = initialize_myc_state(&secondary, &primary_metadata, applied_at, &build)
    206         .await
    207         .expect_err("cross-instance metadata");
    208     assert_eq!(mismatch.kind(), MycStateHostErrorKind::InvalidEvidence);
    209     assert_eq!(mismatch.code(), "state_evidence_invalid");
    210     assert!(Error::source(&mismatch).is_none());
    211     let rendered = format!("{mismatch} {mismatch:?}");
    212     assert!(!rendered.contains(directory.path().to_string_lossy().as_ref()));
    213     assert!(!rendered.contains("state.sqlite"));
    214     assert!(!secondary.artifacts().state_database().exists());
    215 }
    216 
    217 #[test]
    218 fn public_lifecycle_source_is_sealed() {
    219     assert!(LIB_SOURCE.contains("mod state_host;"));
    220     assert!(!LIB_SOURCE.contains("pub mod state_host;"));
    221     assert!(HOST_SOURCE.contains("host: ServiceSqliteHost"));
    222     assert!(!HOST_SOURCE.contains("pub host:"));
    223     for required in [
    224         "ServiceSqliteInitializer",
    225         "ServiceSqliteInitializerFuture",
    226         ".state_directory_plan()",
    227         ".and_then(|plan| plan.provision())",
    228     ] {
    229         assert!(
    230             HOST_SOURCE.contains(required),
    231             "missing sealed initialization boundary `{required}`"
    232         );
    233     }
    234     for forbidden in [
    235         "pub fn transaction",
    236         "pub async fn transaction",
    237         "pub fn pool",
    238         "pub fn connection",
    239         "pub fn into_inner",
    240         "pub fn executor",
    241         "MigrationDescriptor::",
    242         "raw_sql",
    243         "CREATE TABLE",
    244         "PRAGMA application_id",
    245         "PathBuf",
    246         "use sqlx::",
    247         "SqliteConnectOptions",
    248         "ConnectOptions",
    249         "create_dir_all",
    250         "try_exists",
    251     ] {
    252         assert!(
    253             !HOST_SOURCE.contains(forbidden),
    254             "found forbidden lifecycle authority `{forbidden}`"
    255         );
    256     }
    257 }