rhi

Coordinated trade for connected markets
git clone https://radroots.dev/git/rhi.git
Log | Files | Refs | README | LICENSE

services_hardening_state_metadata.rs (7055B)


      1 #![forbid(unsafe_code)]
      2 
      3 use std::{error::Error, path::Path};
      4 
      5 use radroots_storage::event::SourceGeneration;
      6 use rhi::{
      7     RHI_ADMIN_CONTRACT_VERSION, RHI_CONFIG_SCHEMA_VERSION, RHI_PROVIDER_CONTRACT_VERSION,
      8     RHI_STATE_APPLICATION_ID, RHI_STATE_BASE_SCHEMA_VERSION, RHI_STATE_SCHEMA_VERSION,
      9     RHI_STATUS_CONTRACT_VERSION, RadrootsHostEnvironment, RadrootsPathResolver, RadrootsPlatform,
     10     RhiConfigProfile, RhiStateMetadata, RhiStateMetadataErrorKind, parse_rhi_cli_v1_from,
     11     parse_rhi_config_v1, resolve_rhi_runtime_context,
     12 };
     13 
     14 const EXAMPLE: &str = include_str!("../contracts/services_hardening/config.v1.example.toml");
     15 const LIB_SOURCE: &str = include_str!("../src/lib.rs");
     16 const METADATA_SOURCE: &str = include_str!("../src/state_metadata.rs");
     17 
     18 fn runtime(root: &Path, profile: &str) -> rhi::RhiRuntimeContext {
     19     let root = root.to_str().expect("UTF-8 temporary root");
     20     let invocation = parse_rhi_cli_v1_from([
     21         "rhi",
     22         "--profile",
     23         profile,
     24         "--instance",
     25         "primary",
     26         "--repo-local-root",
     27         root,
     28         "run",
     29     ])
     30     .expect("valid test invocation");
     31     resolve_rhi_runtime_context(
     32         &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()),
     33         &invocation,
     34     )
     35     .expect("runtime context")
     36 }
     37 
     38 fn state_metadata(
     39     runtime: &rhi::RhiRuntimeContext,
     40     source: &str,
     41 ) -> Result<RhiStateMetadata, rhi::RhiStateMetadataError> {
     42     let configuration =
     43         parse_rhi_config_v1(source.as_bytes(), RhiConfigProfile::RepoLocal).expect("configuration");
     44     RhiStateMetadata::new(
     45         runtime,
     46         &configuration,
     47         SourceGeneration::new([0x5a; 32]).expect("generation"),
     48         1_725_000_000_000,
     49     )
     50 }
     51 
     52 #[test]
     53 fn exact_database_configuration_identity_and_policy_bindings_are_frozen() {
     54     let directory = tempfile::tempdir().expect("temporary root");
     55     let runtime = runtime(directory.path(), "repo-local");
     56     let metadata = state_metadata(&runtime, EXAMPLE).expect("state metadata");
     57     let database = metadata.database();
     58 
     59     assert_eq!(RHI_STATE_APPLICATION_ID.to_be_bytes(), *b"RDRH");
     60     assert_eq!(database.application_id().get(), RHI_STATE_APPLICATION_ID);
     61     assert_eq!(database.service().as_str(), "rhi");
     62     assert_eq!(database.instance().as_str(), "primary");
     63     assert_eq!(database.source_generation().as_bytes(), &[0x5a; 32]);
     64     assert_eq!(
     65         database.state_schema_version().get(),
     66         RHI_STATE_BASE_SCHEMA_VERSION
     67     );
     68     assert_eq!(
     69         metadata
     70             .database_identity()
     71             .supported_state_schema_version()
     72             .get(),
     73         RHI_STATE_SCHEMA_VERSION
     74     );
     75     assert_eq!(database.created_at_unix_ms(), 1_725_000_000_000);
     76     assert_eq!(metadata.expected_identity().as_hex(), "2".repeat(64));
     77 
     78     let versions = metadata.policy_versions();
     79     assert_eq!(versions.configuration(), RHI_CONFIG_SCHEMA_VERSION);
     80     assert_eq!(versions.state(), RHI_STATE_SCHEMA_VERSION);
     81     assert_eq!(versions.admin(), RHI_ADMIN_CONTRACT_VERSION);
     82     assert_eq!(versions.status(), RHI_STATUS_CONTRACT_VERSION);
     83     assert_eq!(versions.provider(), RHI_PROVIDER_CONTRACT_VERSION);
     84     assert_eq!(
     85         lower_hex(metadata.configuration_digest().as_bytes()),
     86         "7950e77614e1302f673d3434a58a69bfb4ce9c8006b61b29a12deb2b729136d4"
     87     );
     88     assert_eq!(
     89         lower_hex(metadata.evidence_policy_digest().as_bytes()),
     90         "43f083e29fcff4f90e66b546c49f74b0205ecb148beb352adb5a211d3e5f86b2"
     91     );
     92 }
     93 
     94 #[test]
     95 fn digests_use_fully_defaulted_values_and_change_with_normalized_policy() {
     96     let directory = tempfile::tempdir().expect("temporary root");
     97     let runtime = runtime(directory.path(), "repo-local");
     98     let explicit = state_metadata(&runtime, EXAMPLE).expect("explicit defaults");
     99     let implicit_source = EXAMPLE
    100         .replace("shutdown_grace_ms = 30000\n", "")
    101         .replace("level = \"info\"\n", "")
    102         .replace("format = \"json\"\n", "")
    103         .replace("busy_timeout_ms = 5000\n", "")
    104         .replace("max_connections = 8\n", "");
    105     let implicit = state_metadata(&runtime, &implicit_source).expect("implicit defaults");
    106     assert_eq!(
    107         explicit.configuration_digest(),
    108         implicit.configuration_digest()
    109     );
    110     assert_eq!(
    111         explicit.evidence_policy_digest(),
    112         implicit.evidence_policy_digest()
    113     );
    114 
    115     let changed = state_metadata(
    116         &runtime,
    117         &EXAMPLE.replace("deadline_ms = 10000", "deadline_ms = 10001"),
    118     )
    119     .expect("changed policy");
    120     assert_ne!(
    121         explicit.configuration_digest(),
    122         changed.configuration_digest()
    123     );
    124     assert_ne!(
    125         explicit.evidence_policy_digest(),
    126         changed.evidence_policy_digest()
    127     );
    128 }
    129 
    130 #[test]
    131 fn profile_binding_fails_closed_without_state_or_source_disclosure() {
    132     let directory = tempfile::tempdir().expect("temporary root");
    133     let runtime = runtime(directory.path(), "repo-local");
    134     let production = parse_rhi_config_v1(EXAMPLE.as_bytes(), RhiConfigProfile::Production)
    135         .expect("production configuration");
    136     let error = RhiStateMetadata::new(
    137         &runtime,
    138         &production,
    139         SourceGeneration::new([0x5a; 32]).expect("generation"),
    140         1,
    141     )
    142     .expect_err("profile mismatch");
    143     assert_eq!(error.kind(), RhiStateMetadataErrorKind::Profile);
    144     assert!(Error::source(&error).is_none());
    145     let rendered = format!("{error} {error:?}");
    146     assert!(!rendered.contains(directory.path().to_string_lossy().as_ref()));
    147     assert!(!rendered.contains(&"2".repeat(64)));
    148 }
    149 
    150 #[test]
    151 fn metadata_debug_and_package_boundary_disclose_no_values() {
    152     let directory = tempfile::tempdir().expect("temporary root");
    153     let runtime = runtime(directory.path(), "repo-local");
    154     let metadata = state_metadata(&runtime, EXAMPLE).expect("state metadata");
    155     let rendered = format!("{metadata:?}");
    156     for forbidden in [
    157         directory.path().to_string_lossy().as_ref(),
    158         &"2".repeat(64),
    159         &lower_hex(metadata.configuration_digest().as_bytes()),
    160         &lower_hex(metadata.evidence_policy_digest().as_bytes()),
    161     ] {
    162         assert!(!rendered.contains(forbidden));
    163     }
    164 
    165     assert!(LIB_SOURCE.contains("mod state_metadata;"));
    166     assert!(!LIB_SOURCE.contains("pub mod state_metadata;"));
    167     for forbidden in [
    168         "sqlx::",
    169         "rusqlite",
    170         "CREATE TABLE",
    171         "INSERT INTO",
    172         "UPDATE ",
    173         "DELETE FROM",
    174         "std::fs",
    175         "std::env",
    176         "std::time",
    177         "Serialize",
    178         "Deserialize",
    179     ] {
    180         assert!(
    181             !METADATA_SOURCE.contains(forbidden),
    182             "found forbidden metadata authority `{forbidden}`"
    183         );
    184     }
    185 }
    186 
    187 fn lower_hex(bytes: &[u8]) -> String {
    188     const DIGITS: &[u8; 16] = b"0123456789abcdef";
    189     let mut output = String::with_capacity(bytes.len() * 2);
    190     for byte in bytes {
    191         output.push(char::from(DIGITS[usize::from(byte >> 4)]));
    192         output.push(char::from(DIGITS[usize::from(byte & 0x0f)]));
    193     }
    194     output
    195 }