myc

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

services_hardening_state_metadata.rs (8193B)


      1 #![forbid(unsafe_code)]
      2 
      3 use std::{error::Error, path::Path};
      4 
      5 use myc::{
      6     MYC_CONFIG_SCHEMA_VERSION, MYC_OPERATOR_CONTRACT_VERSION, MYC_SIGNER_STATUS_CONTRACT_VERSION,
      7     MYC_STATE_APPLICATION_ID, MYC_STATE_BASE_SCHEMA_VERSION, MYC_STATE_SCHEMA_VERSION,
      8     MycConfigProfile, MycStateMetadata, MycStateMetadataErrorKind, RadrootsHostEnvironment,
      9     RadrootsPathResolver, RadrootsPlatform, parse_myc_cli_v1_from, parse_myc_config_v1,
     10     resolve_myc_runtime_context,
     11 };
     12 use radroots_storage::event::SourceGeneration;
     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) -> myc::MycRuntimeContext {
     19     let root = root.to_str().expect("UTF-8 temporary root");
     20     let invocation = parse_myc_cli_v1_from([
     21         "myc",
     22         "--profile",
     23         profile,
     24         "--instance",
     25         "primary",
     26         "--repo-local-root",
     27         root,
     28         "run",
     29     ])
     30     .expect("valid test invocation");
     31     resolve_myc_runtime_context(
     32         &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()),
     33         &invocation,
     34     )
     35     .expect("runtime context")
     36 }
     37 
     38 fn state_metadata(
     39     runtime: &myc::MycRuntimeContext,
     40     source: &str,
     41 ) -> Result<MycStateMetadata, myc::MycStateMetadataError> {
     42     let configuration =
     43         parse_myc_config_v1(source.as_bytes(), MycConfigProfile::RepoLocal).expect("configuration");
     44     MycStateMetadata::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.initial_database_metadata();
     58 
     59     assert_eq!(MYC_STATE_APPLICATION_ID.to_be_bytes(), *b"RDMY");
     60     assert_eq!(database.application_id().get(), MYC_STATE_APPLICATION_ID);
     61     assert_eq!(database.service().as_str(), "myc");
     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         MYC_STATE_BASE_SCHEMA_VERSION
     67     );
     68     assert_eq!(database.created_at_unix_ms(), 1_725_000_000_000);
     69     assert_eq!(
     70         metadata
     71             .database_identity()
     72             .supported_state_schema_version()
     73             .get(),
     74         MYC_STATE_SCHEMA_VERSION
     75     );
     76 
     77     let identities = metadata.expected_identities();
     78     assert_eq!(identities.transport().as_hex(), "4".repeat(64));
     79     assert_eq!(identities.user().as_hex(), "2".repeat(64));
     80     assert_eq!(
     81         identities.discovery().expect("discovery identity").as_hex(),
     82         "3".repeat(64)
     83     );
     84 
     85     let versions = metadata.policy_versions();
     86     assert_eq!(versions.configuration(), MYC_CONFIG_SCHEMA_VERSION);
     87     assert_eq!(versions.state(), MYC_STATE_SCHEMA_VERSION);
     88     assert_eq!(versions.operator(), MYC_OPERATOR_CONTRACT_VERSION);
     89     assert_eq!(versions.status(), MYC_SIGNER_STATUS_CONTRACT_VERSION);
     90     assert_eq!(
     91         hex::encode(metadata.configuration_digest().as_bytes()),
     92         "68f65b32652a4646d33aafc12075ff44548fb7d9f9a96c4eacaa823702d386cc"
     93     );
     94 }
     95 
     96 #[test]
     97 fn digest_uses_fully_defaulted_values_and_changes_with_normalized_policy() {
     98     let directory = tempfile::tempdir().expect("temporary root");
     99     let runtime = runtime(directory.path(), "repo-local");
    100     let explicit = state_metadata(&runtime, EXAMPLE).expect("explicit defaults");
    101     let implicit_source = EXAMPLE
    102         .replace("shutdown_grace_ms = 30000\n", "")
    103         .replace("level = \"info\"\n", "")
    104         .replace("format = \"json\"\n", "")
    105         .replace("busy_timeout_ms = 5000\n", "")
    106         .replace("max_connections = 8\n", "")
    107         .replace(
    108             "[resource_limits.runtime]\nworker_threads = 4\nblocking_threads = 8\n\n",
    109             "",
    110         );
    111     let implicit = state_metadata(&runtime, &implicit_source).expect("implicit defaults");
    112     assert_eq!(
    113         explicit.configuration_digest(),
    114         implicit.configuration_digest()
    115     );
    116 
    117     let changed = state_metadata(
    118         &runtime,
    119         &EXAMPLE.replace("level = \"info\"", "level = \"warn\""),
    120     )
    121     .expect("changed policy");
    122     assert_ne!(
    123         explicit.configuration_digest(),
    124         changed.configuration_digest()
    125     );
    126 
    127     let changed_ingress = state_metadata(
    128         &runtime,
    129         &EXAMPLE.replacen(
    130             "maximum_past_seconds = 120",
    131             "maximum_past_seconds = 121",
    132             1,
    133         ),
    134     )
    135     .expect("changed ingress policy");
    136     assert_ne!(
    137         explicit.configuration_digest(),
    138         changed_ingress.configuration_digest()
    139     );
    140 }
    141 
    142 #[test]
    143 fn disabled_discovery_binds_no_discovery_identity() {
    144     let directory = tempfile::tempdir().expect("temporary root");
    145     let runtime = runtime(directory.path(), "repo-local");
    146     let mut document = toml::from_str::<toml::Value>(EXAMPLE).expect("example TOML");
    147     document["identity"]["discovery"] = toml::Value::Table(
    148         [("enabled".to_owned(), toml::Value::Boolean(false))]
    149             .into_iter()
    150             .collect(),
    151     );
    152     document["discovery"] = toml::Value::Table(
    153         [("enabled".to_owned(), toml::Value::Boolean(false))]
    154             .into_iter()
    155             .collect(),
    156     );
    157     let source = toml::to_string(&document).expect("disabled-discovery TOML");
    158     let metadata = state_metadata(&runtime, &source).expect("state metadata");
    159 
    160     assert!(metadata.expected_identities().discovery().is_none());
    161 }
    162 
    163 #[test]
    164 fn profile_and_invalid_nostr_identity_bindings_fail_closed() {
    165     let directory = tempfile::tempdir().expect("temporary root");
    166     let runtime = runtime(directory.path(), "repo-local");
    167     let production = parse_myc_config_v1(EXAMPLE.as_bytes(), MycConfigProfile::Production)
    168         .expect("production configuration");
    169     let error = MycStateMetadata::new(
    170         &runtime,
    171         &production,
    172         SourceGeneration::new([0x5a; 32]).expect("generation"),
    173         1,
    174     )
    175     .expect_err("profile mismatch");
    176     assert_eq!(error.kind(), MycStateMetadataErrorKind::Profile);
    177     assert!(Error::source(&error).is_none());
    178 
    179     let invalid_role = EXAMPLE.replacen(&"4".repeat(64), &"f".repeat(64), 1);
    180     assert!(
    181         parse_myc_config_v1(invalid_role.as_bytes(), MycConfigProfile::RepoLocal).is_err(),
    182         "an out-of-field role identity must not remain a valid configuration"
    183     );
    184     let invalid_client = EXAMPLE.replacen(&"7".repeat(64), &"f".repeat(64), 1);
    185     assert!(
    186         parse_myc_config_v1(invalid_client.as_bytes(), MycConfigProfile::RepoLocal).is_err(),
    187         "an out-of-field policy identity must not remain a valid configuration"
    188     );
    189 }
    190 
    191 #[test]
    192 fn metadata_debug_error_and_package_boundary_disclose_no_values() {
    193     let directory = tempfile::tempdir().expect("temporary root");
    194     let runtime = runtime(directory.path(), "repo-local");
    195     let metadata = state_metadata(&runtime, EXAMPLE).expect("state metadata");
    196     let rendered = format!("{metadata:?}");
    197     for forbidden in [
    198         directory.path().to_string_lossy().as_ref(),
    199         &"4".repeat(64),
    200         &"2".repeat(64),
    201         &"3".repeat(64),
    202         &hex::encode(metadata.configuration_digest().as_bytes()),
    203     ] {
    204         assert!(!rendered.contains(forbidden));
    205     }
    206 
    207     assert!(LIB_SOURCE.contains("mod state_metadata;"));
    208     assert!(!LIB_SOURCE.contains("pub mod state_metadata;"));
    209     for forbidden in [
    210         "sqlx::",
    211         "rusqlite",
    212         "CREATE TABLE",
    213         "INSERT INTO",
    214         "UPDATE ",
    215         "DELETE FROM",
    216         "std::fs",
    217         "std::env",
    218         "std::time",
    219         "Serialize",
    220         "Deserialize",
    221     ] {
    222         assert!(
    223             !METADATA_SOURCE.contains(forbidden),
    224             "found forbidden metadata authority `{forbidden}`"
    225         );
    226     }
    227 }