services_hardening_publication_contract.rs (6855B)
1 #![forbid(unsafe_code)] 2 3 use std::error::Error; 4 5 use rhi::{ 6 RHI_PUBLICATION_CONTRACT_VERSION, RHI_PUBLICATION_MAX_ATTEMPTS, RHI_PUBLICATION_MAX_TARGETS, 7 RhiConfigProfile, RhiPublicationAuthority, RhiPublicationMode, parse_rhi_config_v1, 8 }; 9 use serde_json::json; 10 11 const CONTRACT: &str = include_str!("../contracts/services_hardening/publication_outbox.v1.json"); 12 const EXAMPLE: &[u8] = include_bytes!("../contracts/services_hardening/config.v1.example.toml"); 13 const LIB_SOURCE: &str = include_str!("../src/lib.rs"); 14 const PUBLICATION_SOURCE: &str = include_str!("../src/publication.rs"); 15 const CATALOG_SOURCE: &str = include_str!("../src/state_catalog.rs"); 16 const README: &str = include_str!("../README"); 17 18 #[test] 19 fn machine_contract_freezes_step_198_authority_and_schema() { 20 let contract: serde_json::Value = serde_json::from_str(CONTRACT).expect("contract"); 21 assert_eq!(contract["schema"], "radroots.rhi.publication-outbox"); 22 assert_eq!(contract["schema_version"], 1); 23 assert_eq!( 24 contract["contract_version"], 25 RHI_PUBLICATION_CONTRACT_VERSION 26 ); 27 // This Step198 contract records the schema version that introduced the 28 // outbox. Later forward-only migrations are governed by their own 29 // contracts and must not silently rewrite this historical evidence. 30 assert_eq!(contract["state_schema_version"], 7); 31 assert_eq!( 32 contract["publication_modes"], 33 json!(["required", "disabled"]) 34 ); 35 assert_eq!( 36 contract["authority"]["required"]["target_count"]["maximum"], 37 RHI_PUBLICATION_MAX_TARGETS 38 ); 39 assert_eq!( 40 contract["schema_objects"]["immutable"], 41 json!([ 42 "evidence_manifests", 43 "trade_projections", 44 "attestation_reports", 45 "signed_attestation_events", 46 "publication_attempts" 47 ]) 48 ); 49 assert_eq!( 50 contract["schema_objects"]["compare_and_swap"], 51 json!(["publication_outbox", "publication_targets"]) 52 ); 53 assert_eq!( 54 contract["schema_objects"]["target_states"], 55 json!([ 56 "pending", 57 "submitted", 58 "accepted", 59 "rejected", 60 "rate_limited", 61 "auth_required", 62 "failed", 63 "unknown" 64 ]) 65 ); 66 assert_eq!(contract["payload"]["signed_bytes_maximum"], 32_768); 67 assert_eq!(contract["effects"]["sqlite_query_or_mutation"], false); 68 assert_eq!(contract["effects"]["relay_or_network"], false); 69 for forbidden in [ 70 "implicit_publication_mode", 71 "disabled_mode_target_or_retry_state", 72 "caller_forged_target", 73 "non_write_relay_target", 74 "mutable_signed_payload", 75 "mutable_target_identity", 76 "raw_upstream_error_text", 77 "raw_sqlite_handle", 78 "relay_io", 79 "event_rebuild", 80 "event_reserialize", 81 "event_resign", 82 "unbounded_queue_or_target_inventory", 83 ] { 84 assert!( 85 contract["forbidden"] 86 .as_array() 87 .expect("forbidden") 88 .iter() 89 .any(|value| value == forbidden), 90 "missing forbidden boundary {forbidden}" 91 ); 92 } 93 } 94 95 #[test] 96 fn canonical_example_has_literal_publication_identities() { 97 let config = parse_rhi_config_v1(EXAMPLE, RhiConfigProfile::Production).expect("config"); 98 let authority = RhiPublicationAuthority::from_config(&config).expect("authority"); 99 assert_eq!(authority.mode(), RhiPublicationMode::Required); 100 assert_eq!(authority.targets().len(), 2); 101 let retry = authority.retry_policy().expect("retry"); 102 assert_eq!(retry.maximum_attempts(), 10); 103 assert!(retry.maximum_attempts() <= RHI_PUBLICATION_MAX_ATTEMPTS); 104 assert_eq!( 105 lower_hex(authority.target_set_sha256()), 106 "fc044570890935bc41f4763b92d0e079ea0288e0a77a03fc1e299a4c830bbd76" 107 ); 108 assert_eq!( 109 lower_hex(authority.authority_sha256()), 110 "6df5c3bf1bbb5c7b57d81bd1ee600677501a05d6e3c3aaf7577d64ffcbd9566b" 111 ); 112 } 113 114 #[test] 115 fn module_and_side_effect_authority_remain_private_and_deferred() { 116 assert!(LIB_SOURCE.contains("mod publication;")); 117 assert!(!LIB_SOURCE.contains("pub mod publication;")); 118 assert!(LIB_SOURCE.contains("RhiPublicationAuthority")); 119 assert!(README.contains("## Explicit publication authority and durable schema")); 120 assert!(README.contains( 121 "[`publication_outbox.v1.json`](contracts/services_hardening/publication_outbox.v1.json)" 122 )); 123 for table in [ 124 "evidence_manifests", 125 "trade_projections", 126 "attestation_reports", 127 "signed_attestation_events", 128 "publication_outbox", 129 "publication_targets", 130 "publication_attempts", 131 ] { 132 assert!(CATALOG_SOURCE.contains(&format!("CREATE TABLE {table}"))); 133 } 134 assert!(CATALOG_SOURCE.contains("OR OLD.state = 'accepted'")); 135 assert!(CATALOG_SOURCE.contains("AND next_attempt_unix_ms IS NOT NULL")); 136 assert!(CATALOG_SOURCE.contains("AND lease_owner IS NOT NULL")); 137 assert!(CATALOG_SOURCE.contains("AND lease_expires_unix_ms IS NOT NULL")); 138 for forbidden in [ 139 "sqlx::", 140 "radroots_transport", 141 "PublicationSink", 142 "SystemTime", 143 "thread_rng", 144 "OsRng", 145 "std::fs", 146 "std::net", 147 "tokio::spawn", 148 "spawn_blocking", 149 "pub fn sqlite", 150 "pub fn transaction", 151 "pub fn connection", 152 "pub fn into_inner", 153 ] { 154 assert!( 155 !PUBLICATION_SOURCE.contains(forbidden), 156 "premature publication authority {forbidden}" 157 ); 158 } 159 } 160 161 #[test] 162 fn public_authority_debug_and_errors_reveal_no_targets_or_digests() { 163 let config = parse_rhi_config_v1(EXAMPLE, RhiConfigProfile::Production).expect("config"); 164 let authority = RhiPublicationAuthority::from_config(&config).expect("authority"); 165 let rendered = format!("{authority:?}"); 166 assert!(!rendered.contains("relay-primary")); 167 assert!(!rendered.contains(&lower_hex(authority.authority_sha256()))); 168 169 let invalid = core::str::from_utf8(EXAMPLE) 170 .expect("utf8") 171 .replace("mode = \"required\"", "mode = \"invalid-secret\""); 172 let error = parse_rhi_config_v1(invalid.as_bytes(), RhiConfigProfile::Production) 173 .expect_err("invalid config"); 174 let rendered = format!("{error} {error:?}"); 175 assert!(!rendered.contains("invalid-secret")); 176 assert!(Error::source(&error).is_none()); 177 } 178 179 fn lower_hex(bytes: &[u8]) -> String { 180 const DIGITS: &[u8; 16] = b"0123456789abcdef"; 181 let mut output = String::with_capacity(bytes.len() * 2); 182 for byte in bytes { 183 output.push(char::from(DIGITS[usize::from(byte >> 4)])); 184 output.push(char::from(DIGITS[usize::from(byte & 0x0f)])); 185 } 186 output 187 }