rhi.rs (26053B)
1 #![forbid(unsafe_code)] 2 3 //! Exact RHI evidence-attestation wire construction and validation. 4 5 #[cfg(not(feature = "std"))] 6 use alloc::{ 7 boxed::Box, 8 format, 9 string::{String, ToString}, 10 vec, 11 vec::Vec, 12 }; 13 use core::{cmp::Ordering, fmt, num::NonZeroU64}; 14 #[cfg(feature = "std")] 15 use std::{ 16 boxed::Box, 17 format, 18 string::{String, ToString}, 19 vec, 20 vec::Vec, 21 }; 22 23 use radroots_event::{ 24 admission::SignatureVerifiedEvent, 25 envelope::EventEnvelope, 26 envelope::kind::KIND_RHI_EVIDENCE_ATTESTATION, 27 id::{EventId, MutationId, TradeId}, 28 trade::canonical_jcs_value, 29 wire::Nip01EventWireParts, 30 }; 31 use radroots_identity::PublicKey; 32 use serde::Deserialize; 33 use serde_json::Value; 34 use sha2::{Digest as _, Sha256}; 35 36 pub const RADROOTS_RHI_EVIDENCE_ATTESTATION_CONTRACT_ID: &str = 37 "radroots.rhi.evidence_attestation.v1"; 38 pub const RADROOTS_RHI_EVIDENCE_ATTESTATION_MAXIMUM_BYTES: usize = 16 * 1024; 39 const MAXIMUM_TAGS: usize = 7; 40 const MAXIMUM_REASON_CODES: usize = 16; 41 const MAXIMUM_REASON_CODE_BYTES: usize = 64; 42 const STATEMENT_DIGEST_DOMAIN: &[u8] = b"radroots:rhi-evidence-attestation-statement:v1\0"; 43 44 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 45 pub enum RadrootsRhiEvidenceAttestationOutcomeV1 { 46 Valid, 47 Invalid, 48 Indeterminate, 49 } 50 51 impl RadrootsRhiEvidenceAttestationOutcomeV1 { 52 #[must_use] 53 pub const fn as_str(self) -> &'static str { 54 match self { 55 Self::Valid => "valid", 56 Self::Invalid => "invalid", 57 Self::Indeterminate => "indeterminate", 58 } 59 } 60 61 fn topic(self) -> String { 62 format!("radroots:rhi-outcome:{}", self.as_str()) 63 } 64 } 65 66 #[derive(Clone, Copy, PartialEq, Eq)] 67 pub struct RadrootsRhiEvidenceAttestationSupersessionV1 { 68 report_id: [u8; 32], 69 event_id: EventId, 70 } 71 72 impl RadrootsRhiEvidenceAttestationSupersessionV1 { 73 #[must_use] 74 pub const fn report_id(&self) -> &[u8; 32] { 75 &self.report_id 76 } 77 78 #[must_use] 79 pub const fn event_id(&self) -> &EventId { 80 &self.event_id 81 } 82 } 83 84 impl fmt::Debug for RadrootsRhiEvidenceAttestationSupersessionV1 { 85 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 86 formatter.write_str("RadrootsRhiEvidenceAttestationSupersessionV1(<redacted>)") 87 } 88 } 89 90 #[derive(Clone, PartialEq, Eq)] 91 pub struct RadrootsRhiEvidenceAttestationV1 { 92 issuer: PublicKey, 93 trade_id: TradeId, 94 claim_mutation_id: MutationId, 95 outcome: RadrootsRhiEvidenceAttestationOutcomeV1, 96 observed_at_unix_s: u64, 97 trade_generation: NonZeroU64, 98 statement_digest: [u8; 32], 99 supersession: Option<RadrootsRhiEvidenceAttestationSupersessionV1>, 100 canonical_content: Box<str>, 101 } 102 103 impl RadrootsRhiEvidenceAttestationV1 { 104 pub fn from_canonical_content( 105 content: impl AsRef<[u8]>, 106 ) -> Result<Self, RadrootsRhiEvidenceAttestationError> { 107 let content = content.as_ref(); 108 if content.is_empty() || content.len() > RADROOTS_RHI_EVIDENCE_ATTESTATION_MAXIMUM_BYTES { 109 return Err(RadrootsRhiEvidenceAttestationError::NoncanonicalReportContent); 110 } 111 let raw: RawReport = serde_json::from_slice(content) 112 .map_err(|_| RadrootsRhiEvidenceAttestationError::NoncanonicalReportContent)?; 113 validate_fixed_fields(&raw)?; 114 validate_reason_codes(&raw.reason_codes)?; 115 116 let value: Value = serde_json::from_slice(content) 117 .map_err(|_| RadrootsRhiEvidenceAttestationError::NoncanonicalReportContent)?; 118 let object = value 119 .as_object() 120 .ok_or(RadrootsRhiEvidenceAttestationError::NoncanonicalReportContent)?; 121 if !object.contains_key("supersedes_report_id") 122 || !object.contains_key("supersedes_event_id") 123 { 124 return Err(RadrootsRhiEvidenceAttestationError::NoncanonicalReportContent); 125 } 126 127 let issuer = canonical_public_key(&raw.issuer_pubkey)?; 128 let trade_id = canonical_trade_id(&raw.trade_id)?; 129 let claim_mutation_id = canonical_mutation_id(&raw.claim_mutation_id)?; 130 let outcome = parse_outcome(&raw.outcome)?; 131 let trade_generation = NonZeroU64::new(raw.trade_generation) 132 .ok_or(RadrootsRhiEvidenceAttestationError::InvalidReport)?; 133 let report_id = parse_hex_32(&raw.report_id)?; 134 let declared_statement_digest = parse_hex_32(&raw.statement_digest)?; 135 if report_id != declared_statement_digest { 136 return Err(RadrootsRhiEvidenceAttestationError::StatementDigestMismatch); 137 } 138 for digest in [ 139 &raw.projection_digest, 140 &raw.evidence_manifest_digest, 141 &raw.evidence_policy_digest, 142 ] { 143 parse_hex_32(digest)?; 144 } 145 let supersession = parse_supersession( 146 raw.supersedes_report_id.as_deref(), 147 raw.supersedes_event_id.as_deref(), 148 )?; 149 150 let canonical_content = canonical_jcs_value(&value) 151 .map_err(|_| RadrootsRhiEvidenceAttestationError::NoncanonicalReportContent)?; 152 if canonical_content.as_bytes() != content { 153 return Err(RadrootsRhiEvidenceAttestationError::NoncanonicalReportContent); 154 } 155 let mut statement = value; 156 let object = statement 157 .as_object_mut() 158 .ok_or(RadrootsRhiEvidenceAttestationError::NoncanonicalReportContent)?; 159 object.remove("report_id"); 160 object.remove("statement_digest"); 161 let statement_payload = canonical_jcs_value(&statement) 162 .map_err(|_| RadrootsRhiEvidenceAttestationError::NoncanonicalReportContent)?; 163 let mut hasher = Sha256::new(); 164 hasher.update(STATEMENT_DIGEST_DOMAIN); 165 hasher.update(statement_payload.as_bytes()); 166 let computed_statement_digest: [u8; 32] = hasher.finalize().into(); 167 if computed_statement_digest != declared_statement_digest { 168 return Err(RadrootsRhiEvidenceAttestationError::StatementDigestMismatch); 169 } 170 171 Ok(Self { 172 issuer, 173 trade_id, 174 claim_mutation_id, 175 outcome, 176 observed_at_unix_s: raw.observed_at_unix_s, 177 trade_generation, 178 statement_digest: declared_statement_digest, 179 supersession, 180 canonical_content: canonical_content.into_boxed_str(), 181 }) 182 } 183 184 #[must_use] 185 pub const fn issuer(&self) -> &PublicKey { 186 &self.issuer 187 } 188 189 #[must_use] 190 pub const fn trade_id(&self) -> &TradeId { 191 &self.trade_id 192 } 193 194 #[must_use] 195 pub const fn claim_mutation_id(&self) -> &MutationId { 196 &self.claim_mutation_id 197 } 198 199 #[must_use] 200 pub const fn outcome(&self) -> RadrootsRhiEvidenceAttestationOutcomeV1 { 201 self.outcome 202 } 203 204 #[must_use] 205 pub const fn observed_at_unix_s(&self) -> u64 { 206 self.observed_at_unix_s 207 } 208 209 #[must_use] 210 pub const fn trade_generation(&self) -> NonZeroU64 { 211 self.trade_generation 212 } 213 214 #[must_use] 215 pub const fn statement_digest(&self) -> &[u8; 32] { 216 &self.statement_digest 217 } 218 219 #[must_use] 220 pub const fn supersession(&self) -> Option<RadrootsRhiEvidenceAttestationSupersessionV1> { 221 self.supersession 222 } 223 224 #[must_use] 225 pub fn canonical_content(&self) -> &str { 226 &self.canonical_content 227 } 228 } 229 230 impl fmt::Debug for RadrootsRhiEvidenceAttestationV1 { 231 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 232 formatter 233 .debug_struct("RadrootsRhiEvidenceAttestationV1") 234 .field("outcome", &self.outcome) 235 .field("has_supersession", &self.supersession.is_some()) 236 .finish_non_exhaustive() 237 } 238 } 239 240 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 241 pub enum RadrootsRhiEvidenceAttestationError { 242 InvalidAttestationKind, 243 IssuerAuthorMismatch, 244 NoncanonicalReportContent, 245 StatementDigestMismatch, 246 InvalidOutcome, 247 MissingClaimTag, 248 DuplicateTradeTag, 249 DuplicateStatementTag, 250 IncompleteSupersessionReference, 251 StaleTradeGeneration, 252 CallerStructuralTagForbidden, 253 InvalidIdentifier, 254 InvalidReport, 255 InvalidTagShape, 256 UnexpectedTag, 257 ContractTagMismatch, 258 TradeTagMismatch, 259 ClaimTagMismatch, 260 StatementTagMismatch, 261 OutcomeTagMismatch, 262 SupersessionTagMismatch, 263 } 264 265 impl RadrootsRhiEvidenceAttestationError { 266 #[must_use] 267 pub const fn code(self) -> &'static str { 268 match self { 269 Self::InvalidAttestationKind => "invalid_attestation_kind", 270 Self::IssuerAuthorMismatch => "issuer_author_mismatch", 271 Self::NoncanonicalReportContent => "noncanonical_report_content", 272 Self::StatementDigestMismatch => "statement_digest_mismatch", 273 Self::InvalidOutcome => "invalid_outcome", 274 Self::MissingClaimTag => "missing_claim_tag", 275 Self::DuplicateTradeTag => "duplicate_trade_tag", 276 Self::DuplicateStatementTag => "duplicate_statement_tag", 277 Self::IncompleteSupersessionReference => "incomplete_supersession_reference", 278 Self::StaleTradeGeneration => "stale_trade_generation", 279 Self::CallerStructuralTagForbidden => "caller_structural_tag_forbidden", 280 Self::InvalidIdentifier => "invalid_identifier", 281 Self::InvalidReport => "invalid_report", 282 Self::InvalidTagShape => "invalid_tag_shape", 283 Self::UnexpectedTag => "unexpected_tag", 284 Self::ContractTagMismatch => "contract_tag_mismatch", 285 Self::TradeTagMismatch => "trade_tag_mismatch", 286 Self::ClaimTagMismatch => "claim_tag_mismatch", 287 Self::StatementTagMismatch => "statement_tag_mismatch", 288 Self::OutcomeTagMismatch => "outcome_tag_mismatch", 289 Self::SupersessionTagMismatch => "supersession_tag_mismatch", 290 } 291 } 292 } 293 294 impl fmt::Display for RadrootsRhiEvidenceAttestationError { 295 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 296 formatter.write_str(match self { 297 Self::InvalidAttestationKind => "RHI attestation kind is invalid", 298 Self::IssuerAuthorMismatch => "RHI attestation issuer does not match author", 299 Self::NoncanonicalReportContent => "RHI attestation report is not canonical", 300 Self::StatementDigestMismatch => "RHI attestation statement digest does not match", 301 Self::InvalidOutcome => "RHI attestation outcome is invalid", 302 Self::MissingClaimTag => "RHI attestation claim tag is missing", 303 Self::DuplicateTradeTag => "RHI attestation trade tag is duplicated", 304 Self::DuplicateStatementTag => "RHI attestation statement tag is duplicated", 305 Self::IncompleteSupersessionReference => "RHI attestation supersession is incomplete", 306 Self::StaleTradeGeneration => "RHI attestation supersession is stale", 307 Self::CallerStructuralTagForbidden => "caller supplied a governed RHI attestation tag", 308 Self::InvalidIdentifier => "RHI attestation identifier is invalid", 309 Self::InvalidReport => "RHI attestation report is invalid", 310 Self::InvalidTagShape => "RHI attestation tag shape is invalid", 311 Self::UnexpectedTag => "RHI attestation tag is not permitted", 312 Self::ContractTagMismatch => "RHI attestation contract tag does not match", 313 Self::TradeTagMismatch => "RHI attestation trade tag does not match", 314 Self::ClaimTagMismatch => "RHI attestation claim tag does not match", 315 Self::StatementTagMismatch => "RHI attestation statement tag does not match", 316 Self::OutcomeTagMismatch => "RHI attestation outcome tag does not match", 317 Self::SupersessionTagMismatch => "RHI attestation supersession tag does not match", 318 }) 319 } 320 } 321 322 #[cfg(feature = "std")] 323 impl std::error::Error for RadrootsRhiEvidenceAttestationError {} 324 325 pub fn rhi_evidence_attestation_event_build( 326 attestation: &RadrootsRhiEvidenceAttestationV1, 327 ) -> Nip01EventWireParts { 328 Nip01EventWireParts { 329 kind: KIND_RHI_EVIDENCE_ATTESTATION, 330 tags: canonical_tags(attestation), 331 content: attestation.canonical_content().to_string(), 332 } 333 } 334 335 pub fn rhi_evidence_attestation_event_build_with_extra_tags( 336 attestation: &RadrootsRhiEvidenceAttestationV1, 337 extra_tags: &[Vec<String>], 338 ) -> Result<Nip01EventWireParts, RadrootsRhiEvidenceAttestationError> { 339 if extra_tags.iter().any(|tag| { 340 matches!( 341 tag.first().map(String::as_str), 342 Some("contract" | "d" | "x" | "t" | "e") 343 ) 344 }) { 345 return Err(RadrootsRhiEvidenceAttestationError::CallerStructuralTagForbidden); 346 } 347 if !extra_tags.is_empty() { 348 return Err(RadrootsRhiEvidenceAttestationError::UnexpectedTag); 349 } 350 Ok(rhi_evidence_attestation_event_build(attestation)) 351 } 352 353 /// Structurally parses an RHI attestation without claiming signature proof. 354 pub fn rhi_evidence_attestation_from_event( 355 event: &EventEnvelope, 356 ) -> Result<RadrootsRhiEvidenceAttestationV1, RadrootsRhiEvidenceAttestationError> { 357 validate_parts( 358 event.kind_u32(), 359 &event.author().to_hex(), 360 &event.tags_as_vec(), 361 event.content(), 362 ) 363 } 364 365 /// Validates an RHI attestation whose NIP-01 signature is already verified. 366 pub fn rhi_evidence_attestation_from_verified_event( 367 event: &SignatureVerifiedEvent, 368 ) -> Result<RadrootsRhiEvidenceAttestationV1, RadrootsRhiEvidenceAttestationError> { 369 rhi_evidence_attestation_from_event(event.event()) 370 } 371 372 pub fn validate_rhi_evidence_attestation_tags( 373 attestation: &RadrootsRhiEvidenceAttestationV1, 374 tags: &[Vec<String>], 375 ) -> Result<(), RadrootsRhiEvidenceAttestationError> { 376 if tags.len() > MAXIMUM_TAGS { 377 return Err(RadrootsRhiEvidenceAttestationError::InvalidTagShape); 378 } 379 let trade_count = count_unmarked(tags, "d"); 380 if trade_count > 1 { 381 return Err(RadrootsRhiEvidenceAttestationError::DuplicateTradeTag); 382 } 383 let statement_count = count_marked(tags, "x", "statement"); 384 if statement_count > 1 { 385 return Err(RadrootsRhiEvidenceAttestationError::DuplicateStatementTag); 386 } 387 let claim_count = count_marked(tags, "x", "claim"); 388 if claim_count == 0 { 389 return Err(RadrootsRhiEvidenceAttestationError::MissingClaimTag); 390 } 391 let report_count = count_marked(tags, "x", "supersedes_report"); 392 let event_count = count_unmarked(tags, "e"); 393 if report_count != event_count { 394 return Err(RadrootsRhiEvidenceAttestationError::IncompleteSupersessionReference); 395 } 396 if trade_count != 1 397 || statement_count != 1 398 || claim_count != 1 399 || report_count > 1 400 || count_unmarked(tags, "contract") != 1 401 || count_unmarked(tags, "t") != 1 402 || count_named(tags, "x") != claim_count + statement_count + report_count 403 { 404 return Err(RadrootsRhiEvidenceAttestationError::InvalidTagShape); 405 } 406 if tags.iter().any(|tag| { 407 !matches!( 408 tag.first().map(String::as_str), 409 Some("contract" | "d" | "x" | "t" | "e") 410 ) 411 }) { 412 return Err(RadrootsRhiEvidenceAttestationError::UnexpectedTag); 413 } 414 415 if exact_unmarked(tags.first(), "contract")? != RADROOTS_RHI_EVIDENCE_ATTESTATION_CONTRACT_ID { 416 return Err(RadrootsRhiEvidenceAttestationError::ContractTagMismatch); 417 } 418 if canonical_trade_id(exact_unmarked(tags.get(1), "d")?)? != attestation.trade_id { 419 return Err(RadrootsRhiEvidenceAttestationError::TradeTagMismatch); 420 } 421 if canonical_mutation_id(exact_marked(tags.get(2), "x", "claim")?)? 422 != attestation.claim_mutation_id 423 { 424 return Err(RadrootsRhiEvidenceAttestationError::ClaimTagMismatch); 425 } 426 if parse_hex_32(exact_marked(tags.get(3), "x", "statement")?)? != attestation.statement_digest { 427 return Err(RadrootsRhiEvidenceAttestationError::StatementTagMismatch); 428 } 429 if exact_unmarked(tags.get(4), "t")? != attestation.outcome.topic() { 430 return Err(RadrootsRhiEvidenceAttestationError::OutcomeTagMismatch); 431 } 432 match attestation.supersession { 433 None if tags.len() == 5 => Ok(()), 434 Some(supersession) if tags.len() == 7 => { 435 if parse_hex_32(exact_marked(tags.get(5), "x", "supersedes_report")?)? 436 != supersession.report_id 437 || canonical_event_id(exact_unmarked(tags.get(6), "e")?)? != supersession.event_id 438 { 439 return Err(RadrootsRhiEvidenceAttestationError::SupersessionTagMismatch); 440 } 441 Ok(()) 442 } 443 _ => Err(RadrootsRhiEvidenceAttestationError::IncompleteSupersessionReference), 444 } 445 } 446 447 pub fn validate_rhi_evidence_attestation_supersession( 448 current: &RadrootsRhiEvidenceAttestationV1, 449 current_event_id: &EventId, 450 candidate: &RadrootsRhiEvidenceAttestationV1, 451 ) -> Result<(), RadrootsRhiEvidenceAttestationError> { 452 if current.trade_id != candidate.trade_id { 453 return Err(RadrootsRhiEvidenceAttestationError::SupersessionTagMismatch); 454 } 455 let Some(supersession) = candidate.supersession else { 456 return Err(RadrootsRhiEvidenceAttestationError::IncompleteSupersessionReference); 457 }; 458 if supersession.report_id != current.statement_digest 459 || supersession.event_id != *current_event_id 460 { 461 return Err(RadrootsRhiEvidenceAttestationError::SupersessionTagMismatch); 462 } 463 let ordering = candidate 464 .trade_generation 465 .cmp(¤t.trade_generation) 466 .then_with(|| { 467 candidate 468 .observed_at_unix_s 469 .cmp(¤t.observed_at_unix_s) 470 }) 471 .then_with(|| candidate.statement_digest.cmp(¤t.statement_digest)); 472 if ordering != Ordering::Greater { 473 return Err(RadrootsRhiEvidenceAttestationError::StaleTradeGeneration); 474 } 475 Ok(()) 476 } 477 478 pub(crate) fn validate_parts( 479 kind: u32, 480 author: &str, 481 tags: &[Vec<String>], 482 content: &str, 483 ) -> Result<RadrootsRhiEvidenceAttestationV1, RadrootsRhiEvidenceAttestationError> { 484 if kind != KIND_RHI_EVIDENCE_ATTESTATION { 485 return Err(RadrootsRhiEvidenceAttestationError::InvalidAttestationKind); 486 } 487 let attestation = RadrootsRhiEvidenceAttestationV1::from_canonical_content(content)?; 488 if canonical_public_key(author)? != attestation.issuer { 489 return Err(RadrootsRhiEvidenceAttestationError::IssuerAuthorMismatch); 490 } 491 validate_rhi_evidence_attestation_tags(&attestation, tags)?; 492 Ok(attestation) 493 } 494 495 fn canonical_tags(attestation: &RadrootsRhiEvidenceAttestationV1) -> Vec<Vec<String>> { 496 let mut tags = Vec::with_capacity(if attestation.supersession.is_some() { 497 7 498 } else { 499 5 500 }); 501 tags.push(vec![ 502 "contract".to_string(), 503 RADROOTS_RHI_EVIDENCE_ATTESTATION_CONTRACT_ID.to_string(), 504 ]); 505 tags.push(vec!["d".to_string(), attestation.trade_id.to_hex()]); 506 tags.push(vec![ 507 "x".to_string(), 508 attestation.claim_mutation_id.to_hex(), 509 "claim".to_string(), 510 ]); 511 tags.push(vec![ 512 "x".to_string(), 513 hex::encode(attestation.statement_digest), 514 "statement".to_string(), 515 ]); 516 tags.push(vec!["t".to_string(), attestation.outcome.topic()]); 517 if let Some(supersession) = attestation.supersession { 518 tags.push(vec![ 519 "x".to_string(), 520 hex::encode(supersession.report_id), 521 "supersedes_report".to_string(), 522 ]); 523 tags.push(vec!["e".to_string(), supersession.event_id.to_hex()]); 524 } 525 tags 526 } 527 528 fn validate_fixed_fields(raw: &RawReport) -> Result<(), RadrootsRhiEvidenceAttestationError> { 529 if raw.contract_id != RADROOTS_RHI_EVIDENCE_ATTESTATION_CONTRACT_ID 530 || raw.contract_version != 1 531 || raw.reducer_contract_id != "radroots.trade.reducer.v1" 532 || raw.reducer_contract_version != 1 533 || raw.attestation_method != "signed_evidence_snapshot" 534 { 535 return Err(RadrootsRhiEvidenceAttestationError::InvalidReport); 536 } 537 Ok(()) 538 } 539 540 fn validate_reason_codes(codes: &[String]) -> Result<(), RadrootsRhiEvidenceAttestationError> { 541 if codes.is_empty() || codes.len() > MAXIMUM_REASON_CODES { 542 return Err(RadrootsRhiEvidenceAttestationError::InvalidReport); 543 } 544 let mut previous: Option<&str> = None; 545 for code in codes { 546 let bytes = code.as_bytes(); 547 if bytes.is_empty() 548 || bytes.len() > MAXIMUM_REASON_CODE_BYTES 549 || !bytes[0].is_ascii_lowercase() 550 || !bytes[bytes.len() - 1].is_ascii_alphanumeric() 551 || bytes 552 .iter() 553 .any(|byte| !(byte.is_ascii_lowercase() || byte.is_ascii_digit() || *byte == b'_')) 554 || previous.is_some_and(|value| value >= code.as_str()) 555 { 556 return Err(RadrootsRhiEvidenceAttestationError::InvalidReport); 557 } 558 previous = Some(code); 559 } 560 Ok(()) 561 } 562 563 fn parse_outcome( 564 outcome: &str, 565 ) -> Result<RadrootsRhiEvidenceAttestationOutcomeV1, RadrootsRhiEvidenceAttestationError> { 566 match outcome { 567 "valid" => Ok(RadrootsRhiEvidenceAttestationOutcomeV1::Valid), 568 "invalid" => Ok(RadrootsRhiEvidenceAttestationOutcomeV1::Invalid), 569 "indeterminate" => Ok(RadrootsRhiEvidenceAttestationOutcomeV1::Indeterminate), 570 _ => Err(RadrootsRhiEvidenceAttestationError::InvalidOutcome), 571 } 572 } 573 574 fn parse_supersession( 575 report: Option<&str>, 576 event: Option<&str>, 577 ) -> Result<Option<RadrootsRhiEvidenceAttestationSupersessionV1>, RadrootsRhiEvidenceAttestationError> 578 { 579 match (report, event) { 580 (None, None) => Ok(None), 581 (Some(report), Some(event)) => Ok(Some(RadrootsRhiEvidenceAttestationSupersessionV1 { 582 report_id: parse_hex_32(report)?, 583 event_id: canonical_event_id(event)?, 584 })), 585 _ => Err(RadrootsRhiEvidenceAttestationError::IncompleteSupersessionReference), 586 } 587 } 588 589 fn canonical_public_key(value: &str) -> Result<PublicKey, RadrootsRhiEvidenceAttestationError> { 590 let key = PublicKey::from_hex(value) 591 .map_err(|_| RadrootsRhiEvidenceAttestationError::InvalidIdentifier)?; 592 if key.to_hex() != value { 593 return Err(RadrootsRhiEvidenceAttestationError::InvalidIdentifier); 594 } 595 Ok(key) 596 } 597 598 fn canonical_trade_id(value: &str) -> Result<TradeId, RadrootsRhiEvidenceAttestationError> { 599 let id = TradeId::parse(value) 600 .map_err(|_| RadrootsRhiEvidenceAttestationError::InvalidIdentifier)?; 601 if id.to_hex() != value { 602 return Err(RadrootsRhiEvidenceAttestationError::InvalidIdentifier); 603 } 604 Ok(id) 605 } 606 607 fn canonical_mutation_id(value: &str) -> Result<MutationId, RadrootsRhiEvidenceAttestationError> { 608 let id = MutationId::parse(value) 609 .map_err(|_| RadrootsRhiEvidenceAttestationError::InvalidIdentifier)?; 610 if id.to_hex() != value { 611 return Err(RadrootsRhiEvidenceAttestationError::InvalidIdentifier); 612 } 613 Ok(id) 614 } 615 616 fn canonical_event_id(value: &str) -> Result<EventId, RadrootsRhiEvidenceAttestationError> { 617 let id = EventId::parse(value) 618 .map_err(|_| RadrootsRhiEvidenceAttestationError::InvalidIdentifier)?; 619 if id.to_hex() != value { 620 return Err(RadrootsRhiEvidenceAttestationError::InvalidIdentifier); 621 } 622 Ok(id) 623 } 624 625 fn parse_hex_32(value: &str) -> Result<[u8; 32], RadrootsRhiEvidenceAttestationError> { 626 if value.len() != 64 627 || value 628 .bytes() 629 .any(|byte| !(byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))) 630 { 631 return Err(RadrootsRhiEvidenceAttestationError::InvalidIdentifier); 632 } 633 let mut bytes = [0_u8; 32]; 634 hex::decode_to_slice(value, &mut bytes) 635 .map_err(|_| RadrootsRhiEvidenceAttestationError::InvalidIdentifier)?; 636 Ok(bytes) 637 } 638 639 fn count_named(tags: &[Vec<String>], name: &str) -> usize { 640 tags.iter() 641 .filter(|tag| tag.first().map(String::as_str) == Some(name)) 642 .count() 643 } 644 645 fn count_unmarked(tags: &[Vec<String>], name: &str) -> usize { 646 tags.iter() 647 .filter(|tag| tag.len() == 2 && tag.first().map(String::as_str) == Some(name)) 648 .count() 649 } 650 651 fn count_marked(tags: &[Vec<String>], name: &str, marker: &str) -> usize { 652 tags.iter() 653 .filter(|tag| { 654 tag.len() == 3 655 && tag.first().map(String::as_str) == Some(name) 656 && tag.get(2).map(String::as_str) == Some(marker) 657 }) 658 .count() 659 } 660 661 fn exact_unmarked<'a>( 662 tag: Option<&'a Vec<String>>, 663 name: &str, 664 ) -> Result<&'a str, RadrootsRhiEvidenceAttestationError> { 665 let tag = tag.ok_or(RadrootsRhiEvidenceAttestationError::InvalidTagShape)?; 666 if tag.len() != 2 || tag.first().map(String::as_str) != Some(name) { 667 return Err(RadrootsRhiEvidenceAttestationError::InvalidTagShape); 668 } 669 Ok(&tag[1]) 670 } 671 672 fn exact_marked<'a>( 673 tag: Option<&'a Vec<String>>, 674 name: &str, 675 marker: &str, 676 ) -> Result<&'a str, RadrootsRhiEvidenceAttestationError> { 677 let tag = tag.ok_or(RadrootsRhiEvidenceAttestationError::InvalidTagShape)?; 678 if tag.len() != 3 679 || tag.first().map(String::as_str) != Some(name) 680 || tag.get(2).map(String::as_str) != Some(marker) 681 { 682 return Err(RadrootsRhiEvidenceAttestationError::InvalidTagShape); 683 } 684 Ok(&tag[1]) 685 } 686 687 #[derive(Deserialize)] 688 #[serde(deny_unknown_fields)] 689 struct RawReport { 690 attestation_method: String, 691 claim_mutation_id: String, 692 contract_id: String, 693 contract_version: u16, 694 evidence_manifest_digest: String, 695 evidence_policy_digest: String, 696 issuer_pubkey: String, 697 observed_at_unix_s: u64, 698 outcome: String, 699 projection_digest: String, 700 reason_codes: Vec<String>, 701 reducer_contract_id: String, 702 reducer_contract_version: u16, 703 report_id: String, 704 statement_digest: String, 705 supersedes_event_id: Option<String>, 706 supersedes_report_id: Option<String>, 707 trade_generation: u64, 708 trade_id: String, 709 }