services_hardening_trade_ingest.rs (15970B)
1 #![forbid(unsafe_code)] 2 3 use std::error::Error; 4 5 use nostr::{EventBuilder, Keys, Kind, Tag, Timestamp}; 6 use rhi::{ 7 RHI_TRADE_EVENT_EXTRA_FIELD_MAX_COUNT, RHI_TRADE_EVENT_EXTRA_JSON_MAX_BYTES, 8 RHI_TRADE_EVENT_ID_MAX_BYTES, RHI_TRADE_EVENT_PUBLIC_KEY_MAX_BYTES, 9 RHI_TRADE_EVENT_SIGNATURE_MAX_BYTES, RHI_TRADE_INGEST_CONTRACT_VERSION, RhiConfigProfile, 10 RhiTradeMutationAdmissionErrorKind, RhiTradeMutationAdmissionLimits, 11 RhiTradeMutationAuthoredTimePolicy, RhiTradeMutationObservedAtUnixSeconds, 12 admit_rhi_trade_mutation_event, parse_rhi_config_v1, 13 }; 14 use serde_json::{Map, Value, json}; 15 use sha2::{Digest, Sha256}; 16 17 const CONFIG: &str = include_str!("../contracts/services_hardening/config.v1.example.toml"); 18 const CONTRACT: &str = include_str!("../contracts/services_hardening/trade_ingest.v1.json"); 19 const VECTOR: &str = include_str!("../contracts/conformance/vectors/trade_ingest_proposal.v1.json"); 20 21 fn configuration(overrides: &[(&str, usize)]) -> rhi::RhiConfigDocumentV1 { 22 let mut source = CONFIG.to_owned(); 23 for (field, value) in overrides { 24 let prefix = format!("{field} = "); 25 let original = source 26 .lines() 27 .find(|line| line.starts_with(&prefix)) 28 .expect("configured event limit") 29 .to_owned(); 30 source = source.replacen(&original, &format!("{field} = {value}"), 1); 31 } 32 parse_rhi_config_v1(source.as_bytes(), RhiConfigProfile::RepoLocal).expect("test configuration") 33 } 34 35 fn limits(overrides: &[(&str, usize)]) -> RhiTradeMutationAdmissionLimits { 36 RhiTradeMutationAdmissionLimits::from_config(&configuration(overrides)).expect("event limits") 37 } 38 39 fn vector() -> Value { 40 serde_json::from_str(VECTOR).expect("trade-ingest vector") 41 } 42 43 fn valid_wire() -> Vec<u8> { 44 vector()["raw_json"] 45 .as_str() 46 .expect("raw event") 47 .as_bytes() 48 .to_vec() 49 } 50 51 fn observed(value: u64) -> RhiTradeMutationObservedAtUnixSeconds { 52 RhiTradeMutationObservedAtUnixSeconds::new(value).expect("observation") 53 } 54 55 fn policy(value: u64) -> RhiTradeMutationAuthoredTimePolicy { 56 RhiTradeMutationAuthoredTimePolicy::new(value).expect("time policy") 57 } 58 59 fn error(bytes: &[u8]) -> RhiTradeMutationAdmissionErrorKind { 60 admit_rhi_trade_mutation_event(limits(&[]), bytes, observed(1_784_347_200), policy(0)) 61 .expect_err("event must fail") 62 .kind() 63 } 64 65 fn keys(seed: u8) -> Keys { 66 Keys::parse(&format!("{seed:02x}{}", "00".repeat(31))).expect("test keys") 67 } 68 69 fn fixture_keys() -> Keys { 70 Keys::parse("10c5304d6c9ae3a1a16f7860f1cc8f5e3a76225a2663b3a989a0d775919b7df5") 71 .expect("approved fixture keys") 72 } 73 74 fn signed_variant( 75 kind: u16, 76 created_at: u64, 77 content: String, 78 tags: Vec<Tag>, 79 keys: &Keys, 80 ) -> Vec<u8> { 81 let event = EventBuilder::new(Kind::Custom(kind), content) 82 .tags(tags) 83 .custom_created_at(Timestamp::from_secs(created_at)) 84 .sign_with_keys(keys) 85 .expect("signed event"); 86 serde_json::to_vec(&event).expect("event JSON") 87 } 88 89 fn vector_parts() -> (String, Vec<Tag>, u64) { 90 let event: Value = serde_json::from_slice(&valid_wire()).expect("event JSON"); 91 let content = event["content"].as_str().expect("content").to_owned(); 92 let tags = event["tags"] 93 .as_array() 94 .expect("tags") 95 .iter() 96 .map(|tag| { 97 let values = tag 98 .as_array() 99 .expect("tag") 100 .iter() 101 .map(|value| value.as_str().expect("tag element").to_owned()) 102 .collect::<Vec<_>>(); 103 Tag::parse(values).expect("typed tag") 104 }) 105 .collect::<Vec<_>>(); 106 let created_at = event["created_at"].as_u64().expect("created_at"); 107 (content, tags, created_at) 108 } 109 110 #[test] 111 fn machine_contract_vector_and_configuration_projection_are_exact() { 112 let contract: Value = serde_json::from_str(CONTRACT).expect("trade-ingest contract"); 113 assert_eq!(contract["schema"], "radroots.rhi.trade-ingest.v1"); 114 assert_eq!(contract["contract_version"], 1); 115 assert_eq!(RHI_TRADE_INGEST_CONTRACT_VERSION, 1); 116 assert_eq!(contract["wire"]["original_wire_cap_before_parse"], true); 117 assert_eq!( 118 contract["verification"]["registered_kinds"], 119 json!([3470, 3471, 3472, 3473, 3474]) 120 ); 121 assert_eq!(contract["authored_time"]["default"], "none"); 122 assert_eq!(contract["effects"]["sqlite"], false); 123 124 let digest = format!("{:x}", Sha256::digest(VECTOR.as_bytes())); 125 assert_eq!(contract["conformance_vector"]["sha256"], digest); 126 127 let limits = limits(&[]); 128 assert_eq!(limits.wire_bytes(), 262_144); 129 assert_eq!(limits.content_bytes(), 131_072); 130 assert_eq!(limits.tag_count(), 1_024); 131 assert_eq!(limits.tag_total_elements(), 4_096); 132 assert_eq!(limits.tag_element_bytes(), 4_096); 133 assert_eq!(limits.tag_total_bytes(), 131_072); 134 assert_eq!(RHI_TRADE_EVENT_ID_MAX_BYTES, 64); 135 assert_eq!(RHI_TRADE_EVENT_PUBLIC_KEY_MAX_BYTES, 64); 136 assert_eq!(RHI_TRADE_EVENT_SIGNATURE_MAX_BYTES, 128); 137 assert_eq!(RHI_TRADE_EVENT_EXTRA_FIELD_MAX_COUNT, 64); 138 assert_eq!(RHI_TRADE_EVENT_EXTRA_JSON_MAX_BYTES, 65_536); 139 } 140 141 #[test] 142 fn promoted_lib_proposal_vector_is_verified_and_retained_exactly() { 143 let bytes = valid_wire(); 144 let admitted = 145 admit_rhi_trade_mutation_event(limits(&[]), &bytes, observed(1_784_347_200), policy(0)) 146 .expect("canonical signed mutation"); 147 assert_eq!(admitted.original_bytes(), bytes); 148 assert_eq!(admitted.event_id().to_hex(), vector()["event_id"]); 149 assert_eq!(admitted.event_kind(), 3470); 150 assert_eq!(admitted.authored_at_unix_seconds(), 1_784_347_200); 151 assert_eq!( 152 admitted.mutation().mutation_id.as_ref(), 153 Some(admitted.mutation_id()) 154 ); 155 156 let rendered = format!("{admitted:?}"); 157 assert!(!rendered.contains(vector()["event_id"].as_str().expect("event id"))); 158 assert!(!rendered.contains(&admitted.mutation_id().to_hex())); 159 assert!(!rendered.contains("farm-1")); 160 } 161 162 #[test] 163 fn every_configured_wire_limit_is_exact_and_precedes_verification() { 164 let bytes = valid_wire(); 165 let value: Value = serde_json::from_slice(&bytes).expect("event"); 166 let tags = value["tags"].as_array().expect("tags"); 167 let content_bytes = value["content"].as_str().expect("content").len(); 168 let tag_count = tags.len(); 169 let tag_elements = tags 170 .iter() 171 .map(|tag| tag.as_array().expect("tag").len()) 172 .sum::<usize>(); 173 let tag_bytes = tags 174 .iter() 175 .flat_map(|tag| tag.as_array().expect("tag")) 176 .map(|value| value.as_str().expect("element").len()) 177 .sum::<usize>(); 178 let tag_element_bytes = tags 179 .iter() 180 .flat_map(|tag| tag.as_array().expect("tag")) 181 .map(|value| value.as_str().expect("element").len()) 182 .max() 183 .expect("element"); 184 185 for (field, exact, rejected) in [ 186 ( 187 "wire_bytes", 188 bytes.len(), 189 RhiTradeMutationAdmissionErrorKind::EventTooLarge, 190 ), 191 ( 192 "content_bytes", 193 content_bytes, 194 RhiTradeMutationAdmissionErrorKind::EventContentTooLarge, 195 ), 196 ( 197 "tag_count", 198 tag_count, 199 RhiTradeMutationAdmissionErrorKind::TooManyTags, 200 ), 201 ( 202 "tag_total_elements", 203 tag_elements, 204 RhiTradeMutationAdmissionErrorKind::TooManyTagElements, 205 ), 206 ( 207 "tag_element_bytes", 208 tag_element_bytes, 209 RhiTradeMutationAdmissionErrorKind::TagElementTooLarge, 210 ), 211 ( 212 "tag_total_bytes", 213 tag_bytes, 214 RhiTradeMutationAdmissionErrorKind::TagsTooLarge, 215 ), 216 ] { 217 admit_rhi_trade_mutation_event( 218 limits(&[(field, exact)]), 219 &bytes, 220 observed(1_784_347_200), 221 policy(0), 222 ) 223 .unwrap_or_else(|failure| panic!("{field} exact boundary failed: {failure}")); 224 let failure = admit_rhi_trade_mutation_event( 225 limits(&[(field, exact - 1)]), 226 &bytes, 227 observed(1_784_347_200), 228 policy(0), 229 ) 230 .expect_err("just below required capacity"); 231 assert_eq!(failure.kind(), rejected, "{field}"); 232 } 233 } 234 235 #[test] 236 fn original_bytes_identifiers_duplicates_utf8_and_required_shape_fail_closed() { 237 assert_eq!(error(&[]), RhiTradeMutationAdmissionErrorKind::EmptyEvent); 238 assert_eq!( 239 error(&[0xff]), 240 RhiTradeMutationAdmissionErrorKind::InvalidEventUtf8 241 ); 242 243 let oversized = vec![b' '; limits(&[]).wire_bytes() + 1]; 244 assert_eq!( 245 error(&oversized), 246 RhiTradeMutationAdmissionErrorKind::EventTooLarge 247 ); 248 249 let valid = String::from_utf8(valid_wire()).expect("UTF-8 event"); 250 let duplicate = valid.replacen("\"id\":", "\"id\":\"11\",\"id\":", 1); 251 assert_eq!( 252 error(duplicate.as_bytes()), 253 RhiTradeMutationAdmissionErrorKind::DuplicateEventField 254 ); 255 256 let mut value: Value = serde_json::from_str(&valid).expect("event"); 257 value["id"] = Value::String("1".repeat(65)); 258 assert_eq!( 259 error(&serde_json::to_vec(&value).expect("event")), 260 RhiTradeMutationAdmissionErrorKind::EventIdentifierTooLarge 261 ); 262 value["id"] = Value::Null; 263 assert_eq!( 264 error(&serde_json::to_vec(&value).expect("event")), 265 RhiTradeMutationAdmissionErrorKind::MalformedEvent 266 ); 267 } 268 269 #[test] 270 fn outer_extensions_are_bounded_but_never_gain_semantic_authority() { 271 let mut exact: Value = serde_json::from_slice(&valid_wire()).expect("event"); 272 let object = exact.as_object_mut().expect("object"); 273 for index in 0..RHI_TRADE_EVENT_EXTRA_FIELD_MAX_COUNT { 274 object.insert(format!("extension_{index:02}"), json!(index)); 275 } 276 let exact_bytes = serde_json::to_vec(&exact).expect("event"); 277 admit_rhi_trade_mutation_event( 278 limits(&[]), 279 &exact_bytes, 280 observed(1_784_347_200), 281 policy(0), 282 ) 283 .expect("exact extra count"); 284 exact 285 .as_object_mut() 286 .expect("object") 287 .insert("extension_over".to_owned(), json!(true)); 288 assert_eq!( 289 error(&serde_json::to_vec(&exact).expect("event")), 290 RhiTradeMutationAdmissionErrorKind::TooManyExtraFields 291 ); 292 293 let mut exact_bytes_value: Value = serde_json::from_slice(&valid_wire()).expect("event"); 294 exact_bytes_value 295 .as_object_mut() 296 .expect("object") 297 .insert("extra".to_owned(), Value::String("x".repeat(65_526))); 298 admit_rhi_trade_mutation_event( 299 limits(&[]), 300 &serde_json::to_vec(&exact_bytes_value).expect("event"), 301 observed(1_784_347_200), 302 policy(0), 303 ) 304 .expect("exact extra byte budget"); 305 exact_bytes_value["extra"] = Value::String("x".repeat(65_527)); 306 assert_eq!( 307 error(&serde_json::to_vec(&exact_bytes_value).expect("event")), 308 RhiTradeMutationAdmissionErrorKind::ExtraFieldsTooLarge 309 ); 310 } 311 312 #[test] 313 fn event_id_signature_kind_author_content_and_tags_are_independent_checks() { 314 let mut value: Value = serde_json::from_slice(&valid_wire()).expect("event"); 315 value["id"] = Value::String("0".repeat(64)); 316 assert_eq!( 317 error(&serde_json::to_vec(&value).expect("event")), 318 RhiTradeMutationAdmissionErrorKind::InvalidEventId 319 ); 320 321 value = serde_json::from_slice(&valid_wire()).expect("event"); 322 value["sig"] = Value::String("0".repeat(128)); 323 assert_eq!( 324 error(&serde_json::to_vec(&value).expect("event")), 325 RhiTradeMutationAdmissionErrorKind::InvalidSignature 326 ); 327 328 let (content, tags, created_at) = vector_parts(); 329 let unsupported = signed_variant(9_999, created_at, content.clone(), tags.clone(), &keys(9)); 330 assert_eq!( 331 error(&unsupported), 332 RhiTradeMutationAdmissionErrorKind::UnsupportedKind 333 ); 334 let future_unsupported = signed_variant( 335 9_999, 336 created_at + 10, 337 content.clone(), 338 tags.clone(), 339 &keys(9), 340 ); 341 assert_eq!( 342 error(&future_unsupported), 343 RhiTradeMutationAdmissionErrorKind::UnsupportedKind 344 ); 345 346 let wrong_author = signed_variant(3_470, created_at, content.clone(), tags.clone(), &keys(9)); 347 assert_eq!( 348 error(&wrong_author), 349 RhiTradeMutationAdmissionErrorKind::InvalidAuthor 350 ); 351 352 assert_eq!(fixture_keys().public_key().to_hex(), vector()["pubkey"]); 353 let mut noncanonical_content = content.clone(); 354 noncanonical_content.insert(1, ' '); 355 let noncanonical = signed_variant( 356 3_470, 357 created_at, 358 noncanonical_content.clone(), 359 tags.clone(), 360 &fixture_keys(), 361 ); 362 assert_eq!( 363 error(&noncanonical), 364 RhiTradeMutationAdmissionErrorKind::InvalidMutation 365 ); 366 let future_noncanonical = signed_variant( 367 3_470, 368 created_at + 10, 369 noncanonical_content, 370 tags.clone(), 371 &fixture_keys(), 372 ); 373 assert_eq!( 374 error(&future_noncanonical), 375 RhiTradeMutationAdmissionErrorKind::InvalidMutation 376 ); 377 378 let mut duplicate_tags = tags; 379 duplicate_tags.push(Tag::parse(["d", "99999999999999999999999999999999"]).expect("tag")); 380 let duplicate = signed_variant(3_470, created_at, content, duplicate_tags, &fixture_keys()); 381 assert_eq!( 382 error(&duplicate), 383 RhiTradeMutationAdmissionErrorKind::InvalidMutation 384 ); 385 } 386 387 #[test] 388 fn authored_time_policy_is_explicit_inclusive_old_safe_and_overflow_bounded() { 389 assert!(RhiTradeMutationObservedAtUnixSeconds::new(0).is_err()); 390 assert!(RhiTradeMutationObservedAtUnixSeconds::new(i64::MAX as u64).is_ok()); 391 assert!(RhiTradeMutationObservedAtUnixSeconds::new(i64::MAX as u64 + 1).is_err()); 392 assert!(RhiTradeMutationAuthoredTimePolicy::new(i64::MAX as u64).is_ok()); 393 assert!(RhiTradeMutationAuthoredTimePolicy::new(i64::MAX as u64 + 1).is_err()); 394 395 let bytes = valid_wire(); 396 admit_rhi_trade_mutation_event(limits(&[]), &bytes, observed(1_784_347_202), policy(0)) 397 .expect("old lineage event"); 398 admit_rhi_trade_mutation_event(limits(&[]), &bytes, observed(1_784_347_198), policy(2)) 399 .expect("inclusive future boundary"); 400 assert_eq!( 401 admit_rhi_trade_mutation_event(limits(&[]), &bytes, observed(1_784_347_197), policy(2),) 402 .expect_err("excessive future") 403 .kind(), 404 RhiTradeMutationAdmissionErrorKind::AuthoredTimeRejected 405 ); 406 407 let (content, tags, _) = vector_parts(); 408 let unrepresentable = 409 signed_variant(3_470, i64::MAX as u64 + 1, content, tags, &fixture_keys()); 410 assert_eq!( 411 admit_rhi_trade_mutation_event( 412 limits(&[]), 413 &unrepresentable, 414 observed(i64::MAX as u64), 415 policy(0), 416 ) 417 .expect_err("unrepresentable authored time") 418 .kind(), 419 RhiTradeMutationAdmissionErrorKind::InvalidAuthoredTime 420 ); 421 } 422 423 #[test] 424 fn errors_and_accepted_debug_are_source_free_and_redacted() { 425 let secret = "trade-secret-evidence-marker"; 426 let malformed = format!("{{\"content\":\"{secret}\"}}"); 427 let failure = 428 admit_rhi_trade_mutation_event(limits(&[]), malformed.as_bytes(), observed(1), policy(0)) 429 .expect_err("malformed event"); 430 assert!(failure.source().is_none()); 431 for rendered in [failure.to_string(), format!("{failure:?}")] { 432 assert!(!rendered.contains(secret)); 433 assert!(!rendered.contains("content")); 434 assert!(!rendered.contains("serde")); 435 } 436 } 437 438 #[test] 439 fn extra_byte_measurement_matches_the_frozen_member_formula() { 440 let mut object = Map::new(); 441 object.insert("extra".to_owned(), Value::String("x".repeat(65_526))); 442 let encoded = serde_json::to_vec(&Value::Object(object)).expect("JSON"); 443 assert_eq!(encoded.len() - 2, RHI_TRADE_EVENT_EXTRA_JSON_MAX_BYTES); 444 }