rhi_attestation.rs (14425B)
1 #![cfg(feature = "json")] 2 3 use std::error::Error as _; 4 5 use nostr::{EventBuilder, Keys, Kind, Tag, Timestamp}; 6 use radroots_event::{ 7 admission::RawEvent, 8 envelope::{EventEnvelope, EventEnvelopeParts}, 9 id::EventId, 10 trade::canonical_jcs_value, 11 }; 12 use radroots_event_codec::{ 13 decode::rhi::{ 14 RadrootsRhiEvidenceAttestationError, RadrootsRhiEvidenceAttestationV1, 15 rhi_evidence_attestation_from_event, rhi_evidence_attestation_from_verified_event, 16 validate_rhi_evidence_attestation_supersession, validate_rhi_evidence_attestation_tags, 17 }, 18 encode::rhi::{ 19 RADROOTS_RHI_EVIDENCE_ATTESTATION_MAXIMUM_BYTES, rhi_evidence_attestation_event_build, 20 rhi_evidence_attestation_event_build_with_extra_tags, 21 }, 22 verify::Nip01SignatureVerifier, 23 }; 24 use serde_json::Value; 25 use sha2::{Digest as _, Sha256}; 26 27 const VECTORS: &str = include_str!( 28 "../../../contracts/conformance/vectors/rhi/evidence_attestation_decision.v1.json" 29 ); 30 const DIGEST_DOMAIN: &[u8] = b"radroots:rhi-evidence-attestation-statement:v1\0"; 31 32 fn vectors() -> Value { 33 serde_json::from_str(VECTORS).expect("RHI conformance vectors") 34 } 35 36 fn vector<'a>(vectors: &'a Value, id: &str) -> &'a Value { 37 vectors["vectors"] 38 .as_array() 39 .expect("vector list") 40 .iter() 41 .find(|vector| vector["id"] == id) 42 .unwrap_or_else(|| panic!("missing vector {id}")) 43 } 44 45 fn positive_content(vector: &Value) -> &str { 46 vector["expected"]["canonical_event_content_utf8"] 47 .as_str() 48 .expect("canonical event content") 49 } 50 51 fn tags(vector: &Value) -> Vec<Vec<String>> { 52 vector["expected"]["tags"] 53 .as_array() 54 .expect("tag list") 55 .iter() 56 .map(|tag| { 57 tag.as_array() 58 .expect("tag") 59 .iter() 60 .map(|value| value.as_str().expect("tag value").to_owned()) 61 .collect() 62 }) 63 .collect() 64 } 65 66 fn structural_event( 67 kind: u32, 68 author: &str, 69 tags: Vec<Vec<String>>, 70 content: String, 71 ) -> EventEnvelope { 72 EventEnvelope::new(EventEnvelopeParts { 73 id: "0".repeat(64), 74 author: author.to_owned(), 75 created_at: 1_800_000_000, 76 kind, 77 tags, 78 content, 79 sig: "1".repeat(128), 80 }) 81 .expect("structurally valid event") 82 } 83 84 fn canonical_report_from_statement(statement: Value) -> String { 85 let payload = canonical_jcs_value(&statement).expect("canonical statement"); 86 let mut hasher = Sha256::new(); 87 hasher.update(DIGEST_DOMAIN); 88 hasher.update(payload.as_bytes()); 89 let digest = hex::encode(hasher.finalize()); 90 let mut report = statement; 91 let object = report.as_object_mut().expect("statement object"); 92 object.insert("report_id".to_owned(), Value::String(digest.clone())); 93 object.insert("statement_digest".to_owned(), Value::String(digest)); 94 canonical_jcs_value(&report).expect("canonical report") 95 } 96 97 #[test] 98 fn all_frozen_rhi_vectors_execute_their_governed_boundaries() { 99 let vectors = vectors(); 100 let current_vector = vector(&vectors, "rhi_evidence_attestation_current_001"); 101 let superseding_vector = vector(&vectors, "rhi_evidence_attestation_superseding_002"); 102 let current = 103 RadrootsRhiEvidenceAttestationV1::from_canonical_content(positive_content(current_vector)) 104 .expect("current report"); 105 let superseding = RadrootsRhiEvidenceAttestationV1::from_canonical_content(positive_content( 106 superseding_vector, 107 )) 108 .expect("superseding report"); 109 110 for (fixture, attestation) in [ 111 (current_vector, ¤t), 112 (superseding_vector, &superseding), 113 ] { 114 let built = rhi_evidence_attestation_event_build(attestation); 115 assert_eq!(built.kind, 3441); 116 assert_eq!(built.tags, tags(fixture)); 117 assert_eq!(built.content, positive_content(fixture)); 118 validate_rhi_evidence_attestation_tags(attestation, &built.tags) 119 .expect("independent tag validator"); 120 let parsed = rhi_evidence_attestation_from_event(&structural_event( 121 built.kind, 122 &attestation.issuer().to_hex(), 123 built.tags, 124 built.content, 125 )) 126 .expect("structural parser"); 127 assert_eq!(&parsed, attestation); 128 } 129 130 let negative_ids = vectors["vectors"] 131 .as_array() 132 .expect("vectors") 133 .iter() 134 .filter(|vector| vector["kind"] == "rhi.evidence_attestation.invalid") 135 .map(|vector| vector["id"].as_str().expect("vector id")) 136 .collect::<Vec<_>>(); 137 assert_eq!(negative_ids.len(), 11); 138 139 let current_parts = rhi_evidence_attestation_event_build(¤t); 140 let error = rhi_evidence_attestation_from_event(&structural_event( 141 3440, 142 ¤t.issuer().to_hex(), 143 current_parts.tags.clone(), 144 current_parts.content.clone(), 145 )) 146 .expect_err("wrong kind"); 147 assert_vector_error(&vectors, "rhi_evidence_attestation_wrong_kind_003", error); 148 149 let error = rhi_evidence_attestation_from_event(&structural_event( 150 current_parts.kind, 151 "e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af", 152 current_parts.tags.clone(), 153 current_parts.content.clone(), 154 )) 155 .expect_err("wrong author"); 156 assert_vector_error(&vectors, "rhi_evidence_attestation_wrong_author_004", error); 157 158 let error = rhi_evidence_attestation_from_event(&structural_event( 159 current_parts.kind, 160 ¤t.issuer().to_hex(), 161 current_parts.tags.clone(), 162 format!("{} ", current_parts.content), 163 )) 164 .expect_err("noncanonical content"); 165 assert_vector_error( 166 &vectors, 167 "rhi_evidence_attestation_noncanonical_content_005", 168 error, 169 ); 170 171 let mut digest_mismatch: Value = serde_json::from_str(¤t_parts.content).unwrap(); 172 digest_mismatch["statement_digest"] = Value::String("0".repeat(64)); 173 let error = RadrootsRhiEvidenceAttestationV1::from_canonical_content( 174 canonical_jcs_value(&digest_mismatch).unwrap(), 175 ) 176 .expect_err("digest mismatch"); 177 assert_vector_error( 178 &vectors, 179 "rhi_evidence_attestation_digest_mismatch_006", 180 error, 181 ); 182 183 let mut unknown_outcome: Value = serde_json::from_str(¤t_parts.content).unwrap(); 184 unknown_outcome["outcome"] = Value::String("complete".to_owned()); 185 let error = RadrootsRhiEvidenceAttestationV1::from_canonical_content( 186 canonical_jcs_value(&unknown_outcome).unwrap(), 187 ) 188 .expect_err("unknown outcome"); 189 assert_vector_error( 190 &vectors, 191 "rhi_evidence_attestation_unknown_outcome_007", 192 error, 193 ); 194 195 let mut missing_claim = current_parts.tags.clone(); 196 missing_claim.retain(|tag| tag.get(2).map(String::as_str) != Some("claim")); 197 assert_vector_error( 198 &vectors, 199 "rhi_evidence_attestation_missing_claim_tag_008", 200 validate_rhi_evidence_attestation_tags(¤t, &missing_claim).unwrap_err(), 201 ); 202 203 let mut duplicate_trade = current_parts.tags.clone(); 204 duplicate_trade.push(vec!["d".to_owned(), "9".repeat(32)]); 205 assert_vector_error( 206 &vectors, 207 "rhi_evidence_attestation_duplicate_trade_tag_009", 208 validate_rhi_evidence_attestation_tags(¤t, &duplicate_trade).unwrap_err(), 209 ); 210 211 let mut duplicate_statement = current_parts.tags.clone(); 212 duplicate_statement.push(vec!["x".to_owned(), "9".repeat(64), "statement".to_owned()]); 213 assert_vector_error( 214 &vectors, 215 "rhi_evidence_attestation_duplicate_statement_tag_010", 216 validate_rhi_evidence_attestation_tags(¤t, &duplicate_statement).unwrap_err(), 217 ); 218 219 let mut incomplete: Value = serde_json::from_str(¤t_parts.content).unwrap(); 220 incomplete["supersedes_report_id"] = Value::String("7".repeat(64)); 221 let error = RadrootsRhiEvidenceAttestationV1::from_canonical_content( 222 canonical_jcs_value(&incomplete).unwrap(), 223 ) 224 .expect_err("incomplete supersession"); 225 assert_vector_error( 226 &vectors, 227 "rhi_evidence_attestation_incomplete_supersession_011", 228 error, 229 ); 230 231 let mut current_statement = current_vector["input"]["statement_payload"].clone(); 232 current_statement["trade_generation"] = Value::from(8); 233 current_statement["observed_at_unix_s"] = Value::from(1_800_000_100_u64); 234 let ordered_current = RadrootsRhiEvidenceAttestationV1::from_canonical_content( 235 canonical_report_from_statement(current_statement.clone()), 236 ) 237 .unwrap(); 238 let current_event_id = EventId::parse("8".repeat(64)).unwrap(); 239 current_statement["trade_generation"] = Value::from(7); 240 current_statement["observed_at_unix_s"] = Value::from(1_800_000_200_u64); 241 current_statement["supersedes_report_id"] = 242 Value::String(hex::encode(ordered_current.statement_digest())); 243 current_statement["supersedes_event_id"] = Value::String(current_event_id.to_hex()); 244 let stale = RadrootsRhiEvidenceAttestationV1::from_canonical_content( 245 canonical_report_from_statement(current_statement), 246 ) 247 .unwrap(); 248 assert_vector_error( 249 &vectors, 250 "rhi_evidence_attestation_stale_supersession_012", 251 validate_rhi_evidence_attestation_supersession(&ordered_current, ¤t_event_id, &stale) 252 .unwrap_err(), 253 ); 254 255 assert_vector_error( 256 &vectors, 257 "rhi_evidence_attestation_caller_structural_tag_013", 258 rhi_evidence_attestation_event_build_with_extra_tags( 259 ¤t, 260 &[vec!["d".to_owned(), current.trade_id().to_hex()]], 261 ) 262 .unwrap_err(), 263 ); 264 } 265 266 fn assert_vector_error(vectors: &Value, id: &str, error: RadrootsRhiEvidenceAttestationError) { 267 assert_eq!( 268 error.code(), 269 vector(vectors, id)["expected"]["error_code"] 270 .as_str() 271 .expect("error code"), 272 "{id}" 273 ); 274 } 275 276 #[test] 277 fn signed_attestation_requires_and_preserves_the_verified_typestate() { 278 let vectors = vectors(); 279 let base = vector(&vectors, "rhi_evidence_attestation_current_001"); 280 let keys = Keys::parse("0101010101010101010101010101010101010101010101010101010101010101") 281 .expect("fixture keys"); 282 let mut statement = base["input"]["statement_payload"].clone(); 283 statement["issuer_pubkey"] = Value::String(keys.public_key().to_hex()); 284 let report = RadrootsRhiEvidenceAttestationV1::from_canonical_content( 285 canonical_report_from_statement(statement), 286 ) 287 .expect("fixture report"); 288 let parts = rhi_evidence_attestation_event_build(&report); 289 let event = EventBuilder::new(Kind::Custom(parts.kind as u16), parts.content) 290 .tags( 291 parts 292 .tags 293 .into_iter() 294 .map(Tag::parse) 295 .collect::<Result<Vec<_>, _>>() 296 .expect("tags"), 297 ) 298 .custom_created_at(Timestamp::from_secs(1_800_000_000)) 299 .sign_with_keys(&keys) 300 .expect("signed event"); 301 let envelope = EventEnvelope::new(EventEnvelopeParts { 302 id: event.id.to_hex(), 303 author: event.pubkey.to_hex(), 304 created_at: event.created_at.as_secs(), 305 kind: u32::from(event.kind.as_u16()), 306 tags: event 307 .tags 308 .iter() 309 .map(|tag| tag.as_slice().to_vec()) 310 .collect(), 311 content: event.content, 312 sig: event.sig.to_string(), 313 }) 314 .expect("event envelope"); 315 let verified = RawEvent::new(envelope) 316 .verify_id() 317 .expect("verified id") 318 .verify_signature(&Nip01SignatureVerifier) 319 .expect("verified signature"); 320 assert_eq!( 321 rhi_evidence_attestation_from_verified_event(&verified).unwrap(), 322 report 323 ); 324 } 325 326 #[test] 327 fn malformed_shapes_bounds_and_diagnostics_fail_closed() { 328 let vectors = vectors(); 329 let fixture = vector(&vectors, "rhi_evidence_attestation_current_001"); 330 let report = 331 RadrootsRhiEvidenceAttestationV1::from_canonical_content(positive_content(fixture)) 332 .expect("report"); 333 let parts = rhi_evidence_attestation_event_build(&report); 334 335 let mut reordered = parts.tags.clone(); 336 reordered.swap(0, 1); 337 assert_eq!( 338 validate_rhi_evidence_attestation_tags(&report, &reordered).unwrap_err(), 339 RadrootsRhiEvidenceAttestationError::InvalidTagShape 340 ); 341 let mut malformed_marker = parts.tags.clone(); 342 malformed_marker[2][2] = "unknown".to_owned(); 343 assert_eq!( 344 validate_rhi_evidence_attestation_tags(&report, &malformed_marker).unwrap_err(), 345 RadrootsRhiEvidenceAttestationError::MissingClaimTag 346 ); 347 let mut unknown = parts.tags.clone(); 348 unknown.push(vec!["a".to_owned(), "value".to_owned()]); 349 assert_eq!( 350 validate_rhi_evidence_attestation_tags(&report, &unknown).unwrap_err(), 351 RadrootsRhiEvidenceAttestationError::UnexpectedTag 352 ); 353 assert_eq!( 354 RadrootsRhiEvidenceAttestationV1::from_canonical_content(vec![ 355 b'x'; 356 RADROOTS_RHI_EVIDENCE_ATTESTATION_MAXIMUM_BYTES 357 + 1 358 ]) 359 .unwrap_err(), 360 RadrootsRhiEvidenceAttestationError::NoncanonicalReportContent 361 ); 362 let mut missing_supersession_fields: Value = 363 serde_json::from_str(parts.content.as_str()).expect("report JSON"); 364 let object = missing_supersession_fields 365 .as_object_mut() 366 .expect("report object"); 367 object.remove("supersedes_event_id"); 368 object.remove("supersedes_report_id"); 369 assert_eq!( 370 RadrootsRhiEvidenceAttestationV1::from_canonical_content( 371 canonical_jcs_value(&missing_supersession_fields).expect("canonical malformed report") 372 ) 373 .unwrap_err(), 374 RadrootsRhiEvidenceAttestationError::NoncanonicalReportContent 375 ); 376 377 let diagnostic = format!( 378 "{0:?} {0}", 379 RadrootsRhiEvidenceAttestationError::StatementDigestMismatch 380 ); 381 for secret in [ 382 report.issuer().to_hex(), 383 report.trade_id().to_hex(), 384 hex::encode(report.statement_digest()), 385 ] { 386 assert!(!diagnostic.contains(&secret)); 387 } 388 assert!( 389 RadrootsRhiEvidenceAttestationError::StatementDigestMismatch 390 .source() 391 .is_none() 392 ); 393 assert!(!format!("{report:?}").contains(&report.issuer().to_hex())); 394 }