rhi

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

commit a907604040814954fc02fde21a4d1bb1a19f0beb
parent 0d5cba8382782a845fe0789ced633499d72f468a
Author: triesap <tyson@radroots.org>
Date:   Sun, 23 Aug 2026 21:41:15 +0000

test(rhi): close hardened identity wave

Diffstat:
MREADME | 9+++++++++
Atests/services_hardening_wave_100_b.rs | 249+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 258 insertions(+), 0 deletions(-)

diff --git a/README b/README @@ -130,6 +130,15 @@ creates credentials or directories, consults environment or process arguments, or falls back to an adjacent envelope sibling. The former prototype identity and adjacent-key storage APIs are not part of the crate surface. +The wave-two composition proof binds configuration, runtime paths, immutable +state metadata, the encrypted identity, and its separately resolved credential +to one service instance. It proves that state initialization and existing-only +open do not persist the identity secret, credential, encrypted-envelope wire +material, or credential reference in `state.sqlite`. The envelope and +credential remain excluded from the state-backup contract. Actual online +backup, offline restore, and recovery execution remain owned by their later +ordered checkpoint and are not claimed by this boundary proof. + Validate the standalone crate through extbuild: ```text diff --git a/tests/services_hardening_wave_100_b.rs b/tests/services_hardening_wave_100_b.rs @@ -0,0 +1,249 @@ +#![forbid(unsafe_code)] +#![cfg(any(target_os = "linux", target_os = "macos"))] + +use std::{fs, os::unix::fs::PermissionsExt, path::Path}; + +use nostr::{Keys, SecretKey}; +use radroots_service_sqlite::{MigrationAppliedAtUnixSeconds, MigrationBuildIdentity}; +use radroots_storage::event::SourceGeneration; +use rhi::{ + RadrootsHostEnvironment, RadrootsPathResolver, RadrootsPlatform, RhiConfigProfile, + RhiEncryptedIdentityProvisioningMaterial, RhiIdentityEnvelopeBinding, RhiStateMetadata, + initialize_rhi_state, open_rhi_encrypted_identity, open_rhi_state_read_write, + parse_rhi_cli_v1_from, parse_rhi_config_v1, provision_rhi_encrypted_identity, + resolve_rhi_runtime_context, resolve_rhi_wrapping_credential, +}; +use sha2::{Digest, Sha256}; + +const CONFIG_EXAMPLE: &str = include_str!("../contracts/services_hardening/config.v1.example.toml"); +const ENVELOPE_CONTRACT: &str = + include_str!("../contracts/services_hardening/encrypted_identity_envelope.v1.json"); +const CREDENTIAL_CONTRACT: &str = + include_str!("../contracts/services_hardening/wrapping_credential_resolution.v1.json"); +const CONFIG_SOURCE: &str = include_str!("../src/config_v1.rs"); +const CREDENTIAL_SOURCE: &str = include_str!("../src/identity_credential.rs"); +const ENVELOPE_SOURCE: &str = include_str!("../src/identity_envelope.rs"); +const STATE_HOST_SOURCE: &str = include_str!("../src/state_host.rs"); + +fn digest(label: &str) -> [u8; 32] { + Sha256::digest(label.as_bytes()).into() +} + +fn identity_secret() -> [u8; 32] { + let mut candidate = digest("radroots.rhi.wave-100-b.identity-secret.v1"); + while SecretKey::from_slice(&candidate).is_err() { + candidate = Sha256::digest(candidate).into(); + } + candidate +} + +fn runtime(root: &Path, instance: &str) -> rhi::RhiRuntimeContext { + let root = root.to_str().expect("UTF-8 temporary root"); + let invocation = parse_rhi_cli_v1_from([ + "rhi", + "--profile", + "repo-local", + "--instance", + instance, + "--repo-local-root", + root, + "run", + ]) + .expect("valid repo-local invocation"); + resolve_rhi_runtime_context( + &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()), + &invocation, + ) + .expect("runtime context") +} + +fn configuration( + runtime: &rhi::RhiRuntimeContext, + expected_identity: &str, +) -> rhi::RhiConfigDocumentV1 { + let source = CONFIG_EXAMPLE + .replace( + "/var/lib/radroots/services/rhi/default/secrets/service.identity.ncrypt", + runtime + .identity_path() + .to_str() + .expect("UTF-8 identity artifact path"), + ) + .replace(&"2".repeat(64), expected_identity); + parse_rhi_config_v1(source.as_bytes(), RhiConfigProfile::RepoLocal) + .expect("wave-two configuration") +} + +fn prepare_secure_directory(path: &Path) { + fs::create_dir_all(path).expect("secure directory"); + fs::set_permissions(path, fs::Permissions::from_mode(0o700)).expect("secure mode"); +} + +fn contains_bytes(haystack: &[u8], needle: &[u8]) -> bool { + !needle.is_empty() + && haystack + .windows(needle.len()) + .any(|window| window == needle) +} + +fn migration_evidence() -> (MigrationAppliedAtUnixSeconds, MigrationBuildIdentity) { + let applied_at = MigrationAppliedAtUnixSeconds::new(1_725_000_000).expect("migration time"); + let build = MigrationBuildIdentity::new( + env!("CARGO_PKG_VERSION"), + "1111111111111111111111111111111111111111", + "7d7b454b4c9ed86569671993bd03ca868b676665", + "rustc-test", + "test-target", + "service-host", + 1, + 1, + 1, + 1, + 1, + ) + .expect("build identity"); + (applied_at, build) +} + +#[tokio::test] +async fn wave_two_composes_one_runtime_without_crossing_secret_or_state_authority() { + let directory = tempfile::tempdir().expect("temporary root"); + let runtime = runtime(directory.path(), "primary"); + let secret = identity_secret(); + let expected_identity = Keys::new(SecretKey::from_slice(&secret).expect("identity secret")) + .public_key() + .to_hex(); + let configuration = configuration(&runtime, &expected_identity); + let metadata = RhiStateMetadata::new( + &runtime, + &configuration, + SourceGeneration::new([0x6b; 32]).expect("source generation"), + 1_725_000_000_000, + ) + .expect("state metadata"); + let binding = RhiIdentityEnvelopeBinding::from_configuration(&configuration, &metadata) + .expect("identity binding"); + + prepare_secure_directory(runtime.context().paths().secrets()); + let credential_bytes = digest("radroots.rhi.wave-100-b.wrapping-credential.v1"); + let credential_path = runtime + .context() + .paths() + .secrets() + .join("service_wrapping_key"); + fs::write(&credential_path, credential_bytes).expect("credential artifact"); + fs::set_permissions(&credential_path, fs::Permissions::from_mode(0o600)) + .expect("credential mode"); + let credential = + resolve_rhi_wrapping_credential(&runtime, &binding).expect("credential resolution"); + + let material = RhiEncryptedIdentityProvisioningMaterial::new( + secret, + digest("radroots.rhi.wave-100-b.data-key.v1"), + [7; 24], + [9; 24], + ) + .expect("provisioning material"); + let provisioned = provision_rhi_encrypted_identity(&binding, &credential, material) + .expect("create-new identity provisioning"); + assert_eq!(provisioned.public_identity().as_hex(), expected_identity); + let opened = + open_rhi_encrypted_identity(&binding, &credential).expect("existing identity envelope"); + assert_eq!(opened.public_identity().as_hex(), expected_identity); + + prepare_secure_directory(runtime.context().paths().state()); + initialize_rhi_state(&runtime, &metadata) + .await + .expect("create-new state initialization"); + let (applied_at, build) = migration_evidence(); + let state = open_rhi_state_read_write(&runtime, &metadata, applied_at, &build) + .await + .expect("existing state open"); + state.close().await.expect("explicit state close"); + + let state_bytes = fs::read(runtime.artifacts().state_database()).expect("state database"); + for forbidden in [ + secret.as_slice(), + credential_bytes.as_slice(), + b"RRS1".as_slice(), + b"RHWK".as_slice(), + b"service_wrapping_key".as_slice(), + ] { + assert!( + !contains_bytes(&state_bytes, forbidden), + "state database contains protected identity material" + ); + } + assert_eq!( + runtime.identity_path().parent(), + Some(runtime.context().paths().secrets()) + ); + assert_ne!( + runtime.context().paths().state(), + runtime.context().paths().secrets() + ); + assert_eq!( + fs::metadata(runtime.identity_path()) + .expect("identity metadata") + .permissions() + .mode() + & 0o777, + 0o600 + ); + assert_eq!( + fs::metadata(&credential_path) + .expect("credential metadata") + .permissions() + .mode() + & 0o777, + 0o600 + ); +} + +#[test] +fn wave_two_contracts_freeze_backup_exclusion_without_claiming_backup_execution() { + let envelope: serde_json::Value = + serde_json::from_str(ENVELOPE_CONTRACT).expect("envelope contract"); + let credential: serde_json::Value = + serde_json::from_str(CREDENTIAL_CONTRACT).expect("credential contract"); + assert_eq!(envelope["backup"]["state_backup_includes_envelope"], false); + assert_eq!( + envelope["backup"]["state_backup_includes_wrapping_credential"], + false + ); + assert_eq!( + envelope["backup"]["state_backup_includes_plaintext_identity"], + false + ); + assert_eq!(credential["backup_included"], false); + assert!(!STATE_HOST_SOURCE.contains("capture_online_backup")); + assert!(!STATE_HOST_SOURCE.contains("verify_backup_bundle")); + assert!(!STATE_HOST_SOURCE.contains("finalize_staged_restore")); +} + +#[test] +fn wave_two_keeps_configuration_state_identity_and_credential_authorities_separate() { + for forbidden in [ + "resolve_rhi_wrapping_credential", + "RhiWrappingCredential", + "provision_rhi_encrypted_identity", + "open_rhi_encrypted_identity", + "service_wrapping_key", + ] { + assert!(!STATE_HOST_SOURCE.contains(forbidden)); + } + for forbidden in ["sqlx::", "std::fs::", "std::env::"] { + assert!( + !CONFIG_SOURCE.contains(forbidden), + "configuration parser contains forbidden authority {forbidden}" + ); + } + for source in [CREDENTIAL_SOURCE, ENVELOPE_SOURCE] { + for forbidden in ["sqlx::", "rusqlite::", "state.sqlite"] { + assert!(!source.contains(forbidden)); + } + } + let root = Path::new(env!("CARGO_MANIFEST_DIR")); + assert!(!root.join("src/host_identity.rs").exists()); + assert!(!root.join("src/identity_storage.rs").exists()); +}