reconciliation_attestation.rs (26115B)
1 //! Canonical signed attestation construction from one finalization fence. 2 3 use core::fmt; 4 use std::error::Error; 5 6 use nostr::{EventBuilder, Kind, PublicKey as NostrPublicKey, Tag, Timestamp}; 7 use radroots_event::{ 8 envelope::EventEnvelope, 9 id::EventId, 10 wire::{EventWireLimits, Nip01EventWire}, 11 }; 12 use radroots_event_codec::{ 13 authoring::{AuthoredEventBody, AuthoredEventPlan}, 14 decode::rhi::{ 15 RadrootsRhiEvidenceAttestationV1, rhi_evidence_attestation_from_event, 16 validate_rhi_evidence_attestation_supersession, 17 }, 18 }; 19 use radroots_nostr::event::{Verification, verify, verify_id}; 20 use radroots_service_host::{EntropySource, UnixTimeSeconds}; 21 use radroots_trade::evidence::{ 22 RadrootsRhiEvidenceReasonCodeV1, RadrootsRhiEvidenceReportV1, 23 RadrootsRhiEvidenceSupersessionV1, RadrootsTradeEvidenceProjectionDigestV1, 24 }; 25 use sha2::{Digest, Sha256}; 26 use zeroize::Zeroizing; 27 28 use crate::{ 29 RhiDecryptedIdentity, RhiReconciliationCoverage, RhiReconciliationFinalizationFence, 30 RhiReconciliationOutcome, RhiReconciliationReasonCode, 31 }; 32 33 /// Exact version of the signed reconciliation-attestation boundary. 34 pub const RHI_RECONCILIATION_ATTESTATION_CONTRACT_VERSION: u32 = 1; 35 36 /// Exact cap for one canonical signed attestation event. 37 pub const RHI_RECONCILIATION_SIGNED_ATTESTATION_MAX_BYTES: usize = 32 * 1024; 38 39 const MAXIMUM_TAGS: usize = 7; 40 const MAXIMUM_TAG_ELEMENTS: usize = 21; 41 const MAXIMUM_TAG_ELEMENT_BYTES: usize = 128; 42 const MAXIMUM_TAG_BYTES: usize = 1_024; 43 44 /// Stable source-free signed-attestation failure class. 45 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 46 pub enum RhiReconciliationAttestationErrorKind { 47 InvalidInput, 48 IdentityMismatch, 49 EntropyUnavailable, 50 ReportInvalid, 51 EventPlanInvalid, 52 SigningFailed, 53 VerificationFailed, 54 SupersessionInvalid, 55 } 56 57 impl RhiReconciliationAttestationErrorKind { 58 /// Returns the stable machine-readable failure code. 59 #[must_use] 60 pub const fn code(self) -> &'static str { 61 match self { 62 Self::InvalidInput => "reconciliation_attestation_input_invalid", 63 Self::IdentityMismatch => "reconciliation_attestation_identity_mismatch", 64 Self::EntropyUnavailable => "reconciliation_attestation_entropy_unavailable", 65 Self::ReportInvalid => "reconciliation_attestation_report_invalid", 66 Self::EventPlanInvalid => "reconciliation_attestation_event_plan_invalid", 67 Self::SigningFailed => "reconciliation_attestation_signing_failed", 68 Self::VerificationFailed => "reconciliation_attestation_verification_failed", 69 Self::SupersessionInvalid => "reconciliation_attestation_supersession_invalid", 70 } 71 } 72 } 73 74 /// Redacted source-free signed-attestation failure. 75 #[derive(Clone, Copy, PartialEq, Eq)] 76 pub struct RhiReconciliationAttestationError { 77 kind: RhiReconciliationAttestationErrorKind, 78 } 79 80 impl RhiReconciliationAttestationError { 81 /// Returns the stable failure class. 82 #[must_use] 83 pub const fn kind(self) -> RhiReconciliationAttestationErrorKind { 84 self.kind 85 } 86 87 /// Returns the stable machine-readable failure code. 88 #[must_use] 89 pub const fn code(self) -> &'static str { 90 self.kind.code() 91 } 92 } 93 94 impl fmt::Display for RhiReconciliationAttestationError { 95 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 96 formatter.write_str(match self.kind { 97 RhiReconciliationAttestationErrorKind::InvalidInput => { 98 "RHI reconciliation attestation input is invalid" 99 } 100 RhiReconciliationAttestationErrorKind::IdentityMismatch => { 101 "RHI reconciliation attestation identity does not match" 102 } 103 RhiReconciliationAttestationErrorKind::EntropyUnavailable => { 104 "RHI reconciliation attestation entropy is unavailable" 105 } 106 RhiReconciliationAttestationErrorKind::ReportInvalid => { 107 "RHI reconciliation attestation report is invalid" 108 } 109 RhiReconciliationAttestationErrorKind::EventPlanInvalid => { 110 "RHI reconciliation attestation event plan is invalid" 111 } 112 RhiReconciliationAttestationErrorKind::SigningFailed => { 113 "RHI reconciliation attestation signing failed" 114 } 115 RhiReconciliationAttestationErrorKind::VerificationFailed => { 116 "RHI reconciliation attestation verification failed" 117 } 118 RhiReconciliationAttestationErrorKind::SupersessionInvalid => { 119 "RHI reconciliation attestation supersession is invalid" 120 } 121 }) 122 } 123 } 124 125 impl fmt::Debug for RhiReconciliationAttestationError { 126 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 127 formatter 128 .debug_struct("RhiReconciliationAttestationError") 129 .field("kind", &self.kind) 130 .finish() 131 } 132 } 133 134 impl Error for RhiReconciliationAttestationError {} 135 136 /// Sealed exact reference to one prior independently verified attestation. 137 /// 138 /// Callers cannot supply independent report and event identifiers that were 139 /// never proven together. 140 /// 141 /// ```compile_fail 142 /// use rhi::RhiEvidenceAttestationSupersession; 143 /// 144 /// let _forged = RhiEvidenceAttestationSupersession {}; 145 /// ``` 146 pub struct RhiEvidenceAttestationSupersession { 147 report: RadrootsRhiEvidenceReportV1, 148 event_id: EventId, 149 } 150 151 impl RhiEvidenceAttestationSupersession { 152 /// Derives the only public supersession input from one verified result. 153 #[must_use] 154 pub fn from_attestation(attestation: &RhiSignedEvidenceAttestation) -> Self { 155 Self { 156 report: attestation.report.clone(), 157 event_id: EventId::from_bytes(attestation.event_id), 158 } 159 } 160 161 #[cfg(any(test, target_os = "linux", target_os = "macos"))] 162 pub(crate) fn from_persisted( 163 trade_id: &radroots_event::id::TradeId, 164 statement_sha256: [u8; 32], 165 event_id: [u8; 32], 166 canonical_report: &[u8], 167 canonical_event_json: &[u8], 168 ) -> Result<Self, RhiReconciliationAttestationError> { 169 let report = RadrootsRhiEvidenceReportV1::from_canonical_content(canonical_report) 170 .map_err(|_| failure(RhiReconciliationAttestationErrorKind::SupersessionInvalid))?; 171 if report.trade_id() != trade_id 172 || report.statement_digest().as_bytes() != &statement_sha256 173 { 174 return Err(failure( 175 RhiReconciliationAttestationErrorKind::SupersessionInvalid, 176 )); 177 } 178 let source = core::str::from_utf8(canonical_event_json) 179 .map_err(|_| failure(RhiReconciliationAttestationErrorKind::SupersessionInvalid))?; 180 let wire = Nip01EventWire::parse_json_unverified_with_limits(source, signed_event_limits()) 181 .map_err(|_| failure(RhiReconciliationAttestationErrorKind::SupersessionInvalid))?; 182 let event = wire 183 .into_unverified_envelope() 184 .map_err(|_| failure(RhiReconciliationAttestationErrorKind::SupersessionInvalid))?; 185 if verify_id(&event) != Verification::IdVerified 186 || verify(&event) != Verification::Verified 187 || event.id().as_bytes() != &event_id 188 || *event.author() != report.issuer_public_key() 189 { 190 return Err(failure( 191 RhiReconciliationAttestationErrorKind::SupersessionInvalid, 192 )); 193 } 194 let typed = rhi_evidence_attestation_from_event(&event) 195 .map_err(|_| failure(RhiReconciliationAttestationErrorKind::SupersessionInvalid))?; 196 if typed.canonical_content() != report.canonical_content() { 197 return Err(failure( 198 RhiReconciliationAttestationErrorKind::SupersessionInvalid, 199 )); 200 } 201 Ok(Self { 202 report, 203 event_id: EventId::from_bytes(event_id), 204 }) 205 } 206 } 207 208 impl fmt::Debug for RhiEvidenceAttestationSupersession { 209 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 210 formatter.write_str("RhiEvidenceAttestationSupersession([redacted])") 211 } 212 } 213 214 /// Sealed exact report and independently verified signed event. 215 /// 216 /// This value owns its Step195 fence. Step199 consumes it, reruns the fence 217 /// inside its final write transaction, and persists these exact bytes without 218 /// rebuilding or re-signing them. 219 /// 220 /// ```compile_fail 221 /// use rhi::RhiSignedEvidenceAttestation; 222 /// 223 /// let _forged = RhiSignedEvidenceAttestation { event_id: [0; 32] }; 224 /// ``` 225 /// 226 /// ```compile_fail 227 /// use rhi::RhiSignedEvidenceAttestation; 228 /// 229 /// fn require_clone<T: Clone>() {} 230 /// require_clone::<RhiSignedEvidenceAttestation>(); 231 /// ``` 232 pub struct RhiSignedEvidenceAttestation { 233 fence: RhiReconciliationFinalizationFence, 234 report: RadrootsRhiEvidenceReportV1, 235 event_id: [u8; 32], 236 signed_event_bytes: Box<[u8]>, 237 signed_event_sha256: [u8; 32], 238 created_at_unix_seconds: u64, 239 } 240 241 impl RhiSignedEvidenceAttestation { 242 /// Returns the exact RHI signed-attestation contract version. 243 #[must_use] 244 pub const fn contract_version(&self) -> u32 { 245 RHI_RECONCILIATION_ATTESTATION_CONTRACT_VERSION 246 } 247 248 /// Returns the exact canonical report bytes. 249 #[must_use] 250 pub fn canonical_report_bytes(&self) -> &[u8] { 251 self.report.canonical_content().as_bytes() 252 } 253 254 /// Returns the domain-separated statement/report identifier. 255 #[must_use] 256 pub fn statement_digest(&self) -> [u8; 32] { 257 *self.report.statement_digest().as_bytes() 258 } 259 260 /// Returns the independently verified NIP-01 event identifier. 261 #[must_use] 262 pub const fn event_id(&self) -> &[u8; 32] { 263 &self.event_id 264 } 265 266 /// Returns the exact signed event bytes that later persistence must retain. 267 #[must_use] 268 pub fn signed_event_bytes(&self) -> &[u8] { 269 &self.signed_event_bytes 270 } 271 272 /// Returns the SHA-256 digest of the exact signed event bytes. 273 #[must_use] 274 pub const fn signed_event_sha256(&self) -> &[u8; 32] { 275 &self.signed_event_sha256 276 } 277 278 /// Returns the injected NIP-01 authored time, distinct from observation time. 279 #[must_use] 280 pub const fn created_at_unix_seconds(&self) -> u64 { 281 self.created_at_unix_seconds 282 } 283 284 /// Returns the exact evidence coverage retained by the finalization chain. 285 #[must_use] 286 pub const fn coverage(&self) -> RhiReconciliationCoverage { 287 self.fence.evaluation().coverage() 288 } 289 290 /// Returns the exact claim outcome retained by the finalization chain. 291 #[must_use] 292 pub const fn outcome(&self) -> RhiReconciliationOutcome { 293 self.fence.evaluation().outcome() 294 } 295 296 /// Returns whether this report explicitly supersedes one verified predecessor. 297 #[must_use] 298 pub const fn has_supersession(&self) -> bool { 299 self.report.supersession().is_some() 300 } 301 302 pub(crate) const fn fence(&self) -> &RhiReconciliationFinalizationFence { 303 &self.fence 304 } 305 306 pub(crate) const fn issuer_public_key_bytes(&self) -> [u8; 32] { 307 self.report.issuer_public_key().into_bytes() 308 } 309 310 pub(crate) const fn claim_mutation_id_bytes(&self) -> [u8; 32] { 311 *self.report.claim_mutation_id().as_bytes() 312 } 313 314 pub(crate) const fn report_observed_at_unix_seconds(&self) -> u64 { 315 self.report.observed_at_unix_s() 316 } 317 318 pub(crate) fn supersession_bytes(&self) -> Option<([u8; 32], [u8; 32])> { 319 self.report 320 .supersession() 321 .map(|value| (*value.report_id().as_bytes(), *value.event_id().as_bytes())) 322 } 323 } 324 325 impl fmt::Debug for RhiSignedEvidenceAttestation { 326 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 327 formatter 328 .debug_struct("RhiSignedEvidenceAttestation") 329 .field("coverage", &self.coverage()) 330 .field("outcome", &self.outcome()) 331 .field("has_supersession", &self.has_supersession()) 332 .field("signed_event_bytes", &self.signed_event_bytes.len()) 333 .finish_non_exhaustive() 334 } 335 } 336 337 /// Builds, signs, and independently verifies one exact evidence attestation. 338 /// 339 /// Signing consumes exactly 32 bytes from the injected entropy source. No 340 /// clock, entropy, source, relay, persistence, task, or network authority is 341 /// acquired implicitly. 342 pub fn build_rhi_signed_evidence_attestation( 343 fence: RhiReconciliationFinalizationFence, 344 identity: &RhiDecryptedIdentity, 345 created_at: UnixTimeSeconds, 346 entropy: &dyn EntropySource, 347 supersession: Option<RhiEvidenceAttestationSupersession>, 348 ) -> Result<RhiSignedEvidenceAttestation, RhiReconciliationAttestationError> { 349 let evaluation = fence.evaluation(); 350 let projection = evaluation.projection(); 351 let manifest = projection.manifest(); 352 let projection_digest = projection 353 .digest() 354 .ok_or_else(|| failure(RhiReconciliationAttestationErrorKind::InvalidInput))?; 355 let issuer = identity 356 .public_identity() 357 .as_hex() 358 .parse() 359 .map_err(|_| failure(RhiReconciliationAttestationErrorKind::IdentityMismatch))?; 360 let reason_codes = evaluation 361 .reason_codes() 362 .iter() 363 .copied() 364 .map(report_reason) 365 .collect::<Result<Vec<_>, _>>()?; 366 let shared_supersession = supersession.as_ref().map(|prior| { 367 RadrootsRhiEvidenceSupersessionV1::new(prior.report.statement_digest(), prior.event_id) 368 }); 369 let report = RadrootsRhiEvidenceReportV1::new( 370 issuer, 371 *evaluation.claim_mutation_id(), 372 evaluation.outcome(), 373 reason_codes, 374 RadrootsTradeEvidenceProjectionDigestV1::from_bytes(projection_digest), 375 manifest.inner(), 376 shared_supersession, 377 ) 378 .map_err(|_| failure(RhiReconciliationAttestationErrorKind::ReportInvalid))?; 379 report 380 .validate_against_manifest(manifest.inner()) 381 .map_err(|_| failure(RhiReconciliationAttestationErrorKind::ReportInvalid))?; 382 let attestation = 383 RadrootsRhiEvidenceAttestationV1::from_canonical_content(report.canonical_content()) 384 .map_err(|_| failure(RhiReconciliationAttestationErrorKind::ReportInvalid))?; 385 if let Some(prior) = supersession.as_ref() { 386 let current = RadrootsRhiEvidenceAttestationV1::from_canonical_content( 387 prior.report.canonical_content(), 388 ) 389 .map_err(|_| failure(RhiReconciliationAttestationErrorKind::SupersessionInvalid))?; 390 validate_rhi_evidence_attestation_supersession(¤t, &prior.event_id, &attestation) 391 .map_err(|_| failure(RhiReconciliationAttestationErrorKind::SupersessionInvalid))?; 392 } 393 let body = AuthoredEventBody::from_rhi_evidence_attestation(&attestation) 394 .map_err(|_| failure(RhiReconciliationAttestationErrorKind::EventPlanInvalid))?; 395 let plan = AuthoredEventPlan::bind(body, created_at.get(), identity.public_identity().as_hex()) 396 .map_err(|_| failure(RhiReconciliationAttestationErrorKind::EventPlanInvalid))?; 397 398 let mut auxiliary = Zeroizing::new([0_u8; 32]); 399 entropy 400 .fill_bytes(&mut auxiliary[..]) 401 .map_err(|_| failure(RhiReconciliationAttestationErrorKind::EntropyUnavailable))?; 402 let signed_event = sign_plan(identity, &plan, &auxiliary)?; 403 let signed_event_bytes = serde_json::to_vec(&signed_event) 404 .map_err(|_| failure(RhiReconciliationAttestationErrorKind::SigningFailed))?; 405 if signed_event_bytes.len() > RHI_RECONCILIATION_SIGNED_ATTESTATION_MAX_BYTES { 406 return Err(failure( 407 RhiReconciliationAttestationErrorKind::SigningFailed, 408 )); 409 } 410 let verified = verify_signed_event(&plan, &report, manifest.inner(), &signed_event_bytes)?; 411 let signed_event_sha256 = Sha256::digest(&signed_event_bytes).into(); 412 Ok(RhiSignedEvidenceAttestation { 413 fence, 414 report, 415 event_id: *verified.id().as_bytes(), 416 signed_event_bytes: signed_event_bytes.into_boxed_slice(), 417 signed_event_sha256, 418 created_at_unix_seconds: created_at.get(), 419 }) 420 } 421 422 fn report_reason( 423 reason: RhiReconciliationReasonCode, 424 ) -> Result<RadrootsRhiEvidenceReasonCodeV1, RhiReconciliationAttestationError> { 425 RadrootsRhiEvidenceReasonCodeV1::parse(reason.code()) 426 .map_err(|_| failure(RhiReconciliationAttestationErrorKind::ReportInvalid)) 427 } 428 429 fn sign_plan( 430 identity: &RhiDecryptedIdentity, 431 plan: &AuthoredEventPlan, 432 auxiliary: &[u8; 32], 433 ) -> Result<nostr::Event, RhiReconciliationAttestationError> { 434 let kind = u16::try_from(plan.body().kind()) 435 .map(Kind::Custom) 436 .map_err(|_| failure(RhiReconciliationAttestationErrorKind::EventPlanInvalid))?; 437 let tags = plan 438 .body() 439 .tags() 440 .iter() 441 .cloned() 442 .map(Tag::parse) 443 .collect::<Result<Vec<_>, _>>() 444 .map_err(|_| failure(RhiReconciliationAttestationErrorKind::EventPlanInvalid))?; 445 let author = NostrPublicKey::from_hex(identity.public_identity().as_hex()) 446 .map_err(|_| failure(RhiReconciliationAttestationErrorKind::IdentityMismatch))?; 447 let unsigned = EventBuilder::new(kind, plan.body().content()) 448 .tags(tags) 449 .custom_created_at(Timestamp::from_secs(plan.created_at())) 450 .build(author); 451 if unsigned.id.as_ref().map(|event_id| event_id.to_bytes()) 452 != Some(*plan.expected_event_id().as_bytes()) 453 { 454 return Err(failure( 455 RhiReconciliationAttestationErrorKind::EventPlanInvalid, 456 )); 457 } 458 identity 459 .sign_nostr_event(unsigned, auxiliary) 460 .map_err(|_| failure(RhiReconciliationAttestationErrorKind::SigningFailed)) 461 } 462 463 fn verify_signed_event( 464 plan: &AuthoredEventPlan, 465 report: &RadrootsRhiEvidenceReportV1, 466 manifest: &radroots_trade::evidence::RadrootsTradeEvidenceManifestV1, 467 signed_event_bytes: &[u8], 468 ) -> Result<EventEnvelope, RhiReconciliationAttestationError> { 469 let event = verify_signed_event_plan(plan, signed_event_bytes)?; 470 let typed = rhi_evidence_attestation_from_event(&event) 471 .map_err(|_| failure(RhiReconciliationAttestationErrorKind::VerificationFailed))?; 472 if typed.canonical_content() != report.canonical_content() { 473 return Err(failure( 474 RhiReconciliationAttestationErrorKind::VerificationFailed, 475 )); 476 } 477 let reparsed = RadrootsRhiEvidenceReportV1::from_canonical_content(event.content().as_bytes()) 478 .map_err(|_| failure(RhiReconciliationAttestationErrorKind::VerificationFailed))?; 479 reparsed 480 .validate_against_manifest(manifest) 481 .map_err(|_| failure(RhiReconciliationAttestationErrorKind::VerificationFailed))?; 482 if &reparsed != report { 483 return Err(failure( 484 RhiReconciliationAttestationErrorKind::VerificationFailed, 485 )); 486 } 487 Ok(event) 488 } 489 490 fn verify_signed_event_plan( 491 plan: &AuthoredEventPlan, 492 signed_event_bytes: &[u8], 493 ) -> Result<EventEnvelope, RhiReconciliationAttestationError> { 494 if signed_event_bytes.is_empty() 495 || signed_event_bytes.len() > RHI_RECONCILIATION_SIGNED_ATTESTATION_MAX_BYTES 496 { 497 return Err(failure( 498 RhiReconciliationAttestationErrorKind::VerificationFailed, 499 )); 500 } 501 let source = core::str::from_utf8(signed_event_bytes) 502 .map_err(|_| failure(RhiReconciliationAttestationErrorKind::VerificationFailed))?; 503 let wire = Nip01EventWire::parse_json_unverified_with_limits(source, signed_event_limits()) 504 .map_err(|_| failure(RhiReconciliationAttestationErrorKind::VerificationFailed))?; 505 let event = wire 506 .into_unverified_envelope() 507 .map_err(|_| failure(RhiReconciliationAttestationErrorKind::VerificationFailed))?; 508 if verify_id(&event) != Verification::IdVerified || verify(&event) != Verification::Verified { 509 return Err(failure( 510 RhiReconciliationAttestationErrorKind::VerificationFailed, 511 )); 512 } 513 if event.id().as_bytes() != plan.expected_event_id().as_bytes() 514 || event.author().to_hex() != plan.author().to_hex() 515 || event.created_at_u64() != plan.created_at() 516 || event.kind_u32() != plan.body().kind() 517 || event.tags_as_vec() != plan.body().tags() 518 || event.content() != plan.body().content() 519 { 520 return Err(failure( 521 RhiReconciliationAttestationErrorKind::VerificationFailed, 522 )); 523 } 524 Ok(event) 525 } 526 527 const fn signed_event_limits() -> EventWireLimits { 528 EventWireLimits { 529 max_raw_json_bytes: RHI_RECONCILIATION_SIGNED_ATTESTATION_MAX_BYTES, 530 max_content_bytes: 531 radroots_trade::evidence::RADROOTS_RHI_EVIDENCE_REPORT_MAXIMUM_CANONICAL_BYTES, 532 max_tag_count: MAXIMUM_TAGS, 533 max_total_tag_elements: MAXIMUM_TAG_ELEMENTS, 534 max_tag_element_bytes: MAXIMUM_TAG_ELEMENT_BYTES, 535 max_total_tag_bytes: MAXIMUM_TAG_BYTES, 536 max_extra_fields: 0, 537 max_total_extra_json_bytes: 0, 538 } 539 } 540 541 const fn failure(kind: RhiReconciliationAttestationErrorKind) -> RhiReconciliationAttestationError { 542 RhiReconciliationAttestationError { kind } 543 } 544 545 #[cfg(test)] 546 mod tests { 547 use super::*; 548 549 const SIGNED_VECTOR: &str = include_str!( 550 "../contracts/conformance/vectors/reconciliation_attestation_signed_event.v1.json" 551 ); 552 553 fn vector_plan() -> AuthoredEventPlan { 554 let value: serde_json::Value = serde_json::from_str(SIGNED_VECTOR).expect("signed vector"); 555 let content = value["content"].as_str().expect("report content"); 556 let attestation = 557 RadrootsRhiEvidenceAttestationV1::from_canonical_content(content.as_bytes()) 558 .expect("typed report"); 559 AuthoredEventPlan::bind( 560 AuthoredEventBody::from_rhi_evidence_attestation(&attestation).expect("typed body"), 561 value["created_at"].as_u64().expect("authored time"), 562 value["pubkey"].as_str().expect("author"), 563 ) 564 .expect("typed plan") 565 } 566 567 #[test] 568 fn frozen_vector_and_malicious_signer_output_are_independently_verified() { 569 let plan = vector_plan(); 570 verify_signed_event_plan(&plan, SIGNED_VECTOR.trim_end().as_bytes()) 571 .expect("frozen signed vector"); 572 573 let mut wrong_signature: serde_json::Value = 574 serde_json::from_str(SIGNED_VECTOR).expect("signed vector"); 575 wrong_signature["sig"] = serde_json::Value::String("0".repeat(128)); 576 let bytes = serde_json::to_vec(&wrong_signature).expect("malicious signer wire"); 577 assert_eq!( 578 verify_signed_event_plan(&plan, &bytes) 579 .expect_err("malicious signature") 580 .kind(), 581 RhiReconciliationAttestationErrorKind::VerificationFailed 582 ); 583 584 let mut wrong_author: serde_json::Value = 585 serde_json::from_str(SIGNED_VECTOR).expect("signed vector"); 586 wrong_author["pubkey"] = serde_json::Value::String("2".repeat(64)); 587 let bytes = serde_json::to_vec(&wrong_author).expect("wrong-author wire"); 588 assert_eq!( 589 verify_signed_event_plan(&plan, &bytes) 590 .expect_err("wrong author") 591 .kind(), 592 RhiReconciliationAttestationErrorKind::VerificationFailed 593 ); 594 } 595 596 #[test] 597 fn persisted_supersession_revalidates_canonical_report_and_signature() { 598 let value: serde_json::Value = serde_json::from_str(SIGNED_VECTOR).expect("signed vector"); 599 let content = value["content"].as_str().expect("report content"); 600 let report = RadrootsRhiEvidenceReportV1::from_canonical_content(content.as_bytes()) 601 .expect("canonical report"); 602 let event = verify_signed_event_plan(&vector_plan(), SIGNED_VECTOR.trim_end().as_bytes()) 603 .expect("verified event"); 604 let supersession = RhiEvidenceAttestationSupersession::from_persisted( 605 report.trade_id(), 606 *report.statement_digest().as_bytes(), 607 *event.id().as_bytes(), 608 content.as_bytes(), 609 SIGNED_VECTOR.trim_end().as_bytes(), 610 ) 611 .expect("verified persisted supersession"); 612 assert_eq!( 613 format!("{supersession:?}"), 614 "RhiEvidenceAttestationSupersession([redacted])" 615 ); 616 617 for (statement, event_id) in [ 618 ([0; 32], *event.id().as_bytes()), 619 (*report.statement_digest().as_bytes(), [0; 32]), 620 ] { 621 assert_eq!( 622 RhiEvidenceAttestationSupersession::from_persisted( 623 report.trade_id(), 624 statement, 625 event_id, 626 content.as_bytes(), 627 SIGNED_VECTOR.trim_end().as_bytes(), 628 ) 629 .expect_err("mismatched persisted identity") 630 .kind(), 631 RhiReconciliationAttestationErrorKind::SupersessionInvalid 632 ); 633 } 634 } 635 636 #[test] 637 fn every_public_error_class_is_source_free_and_redacted() { 638 for kind in [ 639 RhiReconciliationAttestationErrorKind::InvalidInput, 640 RhiReconciliationAttestationErrorKind::IdentityMismatch, 641 RhiReconciliationAttestationErrorKind::EntropyUnavailable, 642 RhiReconciliationAttestationErrorKind::ReportInvalid, 643 RhiReconciliationAttestationErrorKind::EventPlanInvalid, 644 RhiReconciliationAttestationErrorKind::SigningFailed, 645 RhiReconciliationAttestationErrorKind::VerificationFailed, 646 RhiReconciliationAttestationErrorKind::SupersessionInvalid, 647 ] { 648 let error = failure(kind); 649 assert_eq!(error.kind(), kind); 650 assert!(error.code().starts_with("reconciliation_attestation_")); 651 assert!(Error::source(&error).is_none()); 652 let rendered = format!("{error} {error:?}"); 653 assert!(!rendered.contains("11111111")); 654 assert!(!rendered.contains("f1a2a41d")); 655 assert!(!rendered.contains("1b84c556")); 656 } 657 } 658 }