From 29c0dceefaa2d5cace63ea55c506728b94eaf68c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 00:11:54 -0700 Subject: [PATCH 1/6] test(evidence): require offline capture replay verification --- .../tests/offline_capture_replay.rs | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 crates/originweave-evidence/tests/offline_capture_replay.rs diff --git a/crates/originweave-evidence/tests/offline_capture_replay.rs b/crates/originweave-evidence/tests/offline_capture_replay.rs new file mode 100644 index 00000000..50c52b0d --- /dev/null +++ b/crates/originweave-evidence/tests/offline_capture_replay.rs @@ -0,0 +1,139 @@ +#![allow(clippy::expect_used)] + +use originweave_evidence::{ + CaptureManifest, CaptureManifestValueBinding, CaptureManifestVerificationError, + EvidenceSourceKind, ExtractionCardinality, ExtractionField, ExtractionSchema, + ExtractionSourceChannel, ExtractionValueType, OfflineReplayVerificationError, ProvenanceRecord, + VerificationResult, WarcProvBundle, WarcResourceRecord, verify_offline_capture_package, +}; + +const SOURCE_HASH: &str = "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; +const VALUE_HASH: &str = + "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const DRIFTED_VALUE_HASH: &str = + "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; +const RECORD_ID: &str = "urn:uuid:123e4567-e89b-12d3-a456-426614174000"; +const DATE: &str = "2026-08-26T00:00:00Z"; +const SOFTWARE_COMMIT_SHA: &str = "0123456789abcdef0123456789abcdef01234567"; + +fn schema() -> ExtractionSchema { + let title = ExtractionField::new( + "title", + ExtractionValueType::Text, + ExtractionCardinality::One, + true, + &[ExtractionSourceChannel::NetworkResponse], + ) + .expect("title field"); + ExtractionSchema::new("catalog-v3", vec![title]).expect("schema") +} + +fn resource_record() -> WarcResourceRecord { + let provenance = ProvenanceRecord::new( + "https://example.com/item", + "body", + SOURCE_HASH, + EvidenceSourceKind::NetworkResponse, + VerificationResult::Verified, + ) + .expect("verified provenance"); + WarcResourceRecord::new( + RECORD_ID, + DATE, + "https://example.com/item", + "text/plain", + b"captured-payload".to_vec(), + provenance, + ) + .expect("WARC record") +} + +#[test] +fn offline_replay_verifies_exact_manifest_evidence_and_structured_result() { + let schema = schema(); + let record = resource_record(); + let bundle = WarcProvBundle::new(&record, SOFTWARE_COMMIT_SHA).expect("PROV bundle"); + let value = CaptureManifestValueBinding::new("title", VALUE_HASH, RECORD_ID) + .expect("value binding"); + let manifest = CaptureManifest::new_with_warc_values( + &schema, + &[(&record, &bundle)], + std::slice::from_ref(&value), + ) + .expect("manifest"); + let serialized_manifest = manifest.to_json(); + + let verification = verify_offline_capture_package( + &manifest, + serialized_manifest.as_bytes(), + &schema, + &[(&record, &bundle)], + std::slice::from_ref(&value), + ) + .expect("offline replay verification"); + + assert_eq!(verification.manifest_digest(), manifest.manifest_digest()); + assert_eq!(verification.record_count(), 1); + assert_eq!(verification.value_count(), 1); +} + +#[test] +fn offline_replay_rejects_persisted_manifest_byte_drift_before_evidence_replay() { + let schema = schema(); + let record = resource_record(); + let bundle = WarcProvBundle::new(&record, SOFTWARE_COMMIT_SHA).expect("PROV bundle"); + let value = CaptureManifestValueBinding::new("title", VALUE_HASH, RECORD_ID) + .expect("value binding"); + let manifest = CaptureManifest::new_with_warc_values( + &schema, + &[(&record, &bundle)], + std::slice::from_ref(&value), + ) + .expect("manifest"); + let mut serialized_manifest = manifest.to_json().into_bytes(); + serialized_manifest.push(b' '); + + assert_eq!( + verify_offline_capture_package( + &manifest, + &serialized_manifest, + &schema, + &[(&record, &bundle)], + std::slice::from_ref(&value), + ), + Err(OfflineReplayVerificationError::ManifestBytes( + CaptureManifestVerificationError::IdentityMismatch, + )) + ); +} + +#[test] +fn offline_replay_rejects_structured_result_identity_drift() { + let schema = schema(); + let record = resource_record(); + let bundle = WarcProvBundle::new(&record, SOFTWARE_COMMIT_SHA).expect("PROV bundle"); + let value = CaptureManifestValueBinding::new("title", VALUE_HASH, RECORD_ID) + .expect("value binding"); + let drifted_value = CaptureManifestValueBinding::new("title", DRIFTED_VALUE_HASH, RECORD_ID) + .expect("drifted value binding"); + let manifest = CaptureManifest::new_with_warc_values( + &schema, + &[(&record, &bundle)], + std::slice::from_ref(&value), + ) + .expect("manifest"); + let serialized_manifest = manifest.to_json(); + + assert_eq!( + verify_offline_capture_package( + &manifest, + serialized_manifest.as_bytes(), + &schema, + &[(&record, &bundle)], + std::slice::from_ref(&drifted_value), + ), + Err(OfflineReplayVerificationError::Evidence( + CaptureManifestVerificationError::IdentityMismatch, + )) + ); +} From 0974ff49e1b8e9d1ae43e79707acf3f3b1d30eb4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 00:16:27 -0700 Subject: [PATCH 2/6] test(evidence): canonicalize offline replay regression --- .../tests/offline_capture_replay.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/crates/originweave-evidence/tests/offline_capture_replay.rs b/crates/originweave-evidence/tests/offline_capture_replay.rs index 50c52b0d..e3a2bff3 100644 --- a/crates/originweave-evidence/tests/offline_capture_replay.rs +++ b/crates/originweave-evidence/tests/offline_capture_replay.rs @@ -8,8 +8,7 @@ use originweave_evidence::{ }; const SOURCE_HASH: &str = "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; -const VALUE_HASH: &str = - "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const VALUE_HASH: &str = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; const DRIFTED_VALUE_HASH: &str = "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; const RECORD_ID: &str = "urn:uuid:123e4567-e89b-12d3-a456-426614174000"; @@ -53,8 +52,8 @@ fn offline_replay_verifies_exact_manifest_evidence_and_structured_result() { let schema = schema(); let record = resource_record(); let bundle = WarcProvBundle::new(&record, SOFTWARE_COMMIT_SHA).expect("PROV bundle"); - let value = CaptureManifestValueBinding::new("title", VALUE_HASH, RECORD_ID) - .expect("value binding"); + let value = + CaptureManifestValueBinding::new("title", VALUE_HASH, RECORD_ID).expect("value binding"); let manifest = CaptureManifest::new_with_warc_values( &schema, &[(&record, &bundle)], @@ -82,8 +81,8 @@ fn offline_replay_rejects_persisted_manifest_byte_drift_before_evidence_replay() let schema = schema(); let record = resource_record(); let bundle = WarcProvBundle::new(&record, SOFTWARE_COMMIT_SHA).expect("PROV bundle"); - let value = CaptureManifestValueBinding::new("title", VALUE_HASH, RECORD_ID) - .expect("value binding"); + let value = + CaptureManifestValueBinding::new("title", VALUE_HASH, RECORD_ID).expect("value binding"); let manifest = CaptureManifest::new_with_warc_values( &schema, &[(&record, &bundle)], @@ -112,8 +111,8 @@ fn offline_replay_rejects_structured_result_identity_drift() { let schema = schema(); let record = resource_record(); let bundle = WarcProvBundle::new(&record, SOFTWARE_COMMIT_SHA).expect("PROV bundle"); - let value = CaptureManifestValueBinding::new("title", VALUE_HASH, RECORD_ID) - .expect("value binding"); + let value = + CaptureManifestValueBinding::new("title", VALUE_HASH, RECORD_ID).expect("value binding"); let drifted_value = CaptureManifestValueBinding::new("title", DRIFTED_VALUE_HASH, RECORD_ID) .expect("drifted value binding"); let manifest = CaptureManifest::new_with_warc_values( From b7ae18575c947af6d71f249d86989698be8dc396 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 00:18:12 -0700 Subject: [PATCH 3/6] feat(evidence): verify capture packages offline --- .../src/offline_replay.rs | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 crates/originweave-evidence/src/offline_replay.rs diff --git a/crates/originweave-evidence/src/offline_replay.rs b/crates/originweave-evidence/src/offline_replay.rs new file mode 100644 index 00000000..83407064 --- /dev/null +++ b/crates/originweave-evidence/src/offline_replay.rs @@ -0,0 +1,95 @@ +use std::fmt; + +use crate::{ + CaptureManifest, CaptureManifestValueBinding, CaptureManifestVerificationError, + ExtractionSchema, WarcProvBundle, WarcResourceRecord, +}; + +/// Credential-safe receipt proving one in-memory capture package matched its persisted identity. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct OfflineReplayVerification { + manifest_digest: String, + record_count: usize, + value_count: usize, +} + +impl OfflineReplayVerification { + /// Return the SHA-256 identity of the exact deterministic capture manifest. + #[must_use] + pub fn manifest_digest(&self) -> &str { + &self.manifest_digest + } + + /// Return the number of WARC/PROV record pairs verified by this receipt. + #[must_use] + pub const fn record_count(&self) -> usize { + self.record_count + } + + /// Return the number of schema-bound structured-value identities verified by this receipt. + #[must_use] + pub const fn value_count(&self) -> usize { + self.value_count + } +} + +/// A fail-closed offline capture-package verification failure. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum OfflineReplayVerificationError { + /// Persisted manifest bytes were not the exact deterministic serialization expected in memory. + ManifestBytes(CaptureManifestVerificationError), + /// Schema, WARC/PROV evidence, or structured-value identity did not match the expected manifest. + Evidence(CaptureManifestVerificationError), +} + +impl fmt::Display for OfflineReplayVerificationError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::ManifestBytes(error) => write!( + formatter, + "offline replay persisted manifest bytes failed verification: {error}" + ), + Self::Evidence(error) => write!( + formatter, + "offline replay capture evidence failed verification: {error}" + ), + } + } +} + +impl std::error::Error for OfflineReplayVerificationError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::ManifestBytes(error) | Self::Evidence(error) => Some(error), + } + } +} + +/// Verify one already-materialized capture package without contacting or executing its source. +/// +/// Verification first requires `persisted_manifest_bytes` to equal the expected deterministic +/// manifest serialization byte-for-byte. It then reconstructs and verifies the schema-bound +/// WARC/PROV/value identity through [`CaptureManifest::verify_with_warc_values`]. The operation is +/// deliberately in-memory only: it performs no DNS, network, browser, JavaScript, external-reference, +/// secret, persistence, retention, signing, or authorization action and does not establish factual +/// correctness beyond the supplied evidence contracts. +pub fn verify_offline_capture_package( + expected_manifest: &CaptureManifest, + persisted_manifest_bytes: &[u8], + schema: &ExtractionSchema, + records: &[(&WarcResourceRecord, &WarcProvBundle)], + values: &[CaptureManifestValueBinding], +) -> Result { + expected_manifest + .verify_serialized_json(persisted_manifest_bytes) + .map_err(OfflineReplayVerificationError::ManifestBytes)?; + expected_manifest + .verify_with_warc_values(schema, records, values) + .map_err(OfflineReplayVerificationError::Evidence)?; + + Ok(OfflineReplayVerification { + manifest_digest: expected_manifest.manifest_digest(), + record_count: expected_manifest.records().len(), + value_count: expected_manifest.values().len(), + }) +} From 6b4fa02356ae649388a062f0f09fcdbc4d52aaf5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 00:19:05 -0700 Subject: [PATCH 4/6] feat(evidence): expose offline replay verifier --- crates/originweave-evidence/src/lib.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/originweave-evidence/src/lib.rs b/crates/originweave-evidence/src/lib.rs index 0ca683c0..eb443b87 100644 --- a/crates/originweave-evidence/src/lib.rs +++ b/crates/originweave-evidence/src/lib.rs @@ -9,6 +9,7 @@ mod capture_manifest; mod extraction_schema; +mod offline_replay; mod sensitive_access; mod warc_prov_bundle; mod warc_resource_record; @@ -23,6 +24,9 @@ pub use extraction_schema::{ ExtractionSchemaError, ExtractionSourceChannel, ExtractionValueType, MAX_EXTRACTION_FIELD_COUNT, MAX_EXTRACTION_IDENTIFIER_BYTES, }; +pub use offline_replay::{ + OfflineReplayVerification, OfflineReplayVerificationError, verify_offline_capture_package, +}; pub use sensitive_access::{ MAX_SENSITIVE_FIELD_COUNT, MAX_SENSITIVE_IDENTIFIER_BYTES, SensitiveAccessClass, SensitiveAccessEvidence, SensitiveAccessEvidenceInput, SensitiveAccessOutcome, From f34cdbcd08f58dc7b35ae6a558e2595730bc6d7b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 00:19:41 -0700 Subject: [PATCH 5/6] test(evidence): cover typed offline replay diagnostics --- .../tests/offline_capture_replay.rs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/crates/originweave-evidence/tests/offline_capture_replay.rs b/crates/originweave-evidence/tests/offline_capture_replay.rs index e3a2bff3..6991c8d6 100644 --- a/crates/originweave-evidence/tests/offline_capture_replay.rs +++ b/crates/originweave-evidence/tests/offline_capture_replay.rs @@ -136,3 +136,30 @@ fn offline_replay_rejects_structured_result_identity_drift() { )) ); } + +#[test] +fn offline_replay_errors_preserve_typed_diagnostics_and_sources() { + let manifest_error = OfflineReplayVerificationError::ManifestBytes( + CaptureManifestVerificationError::IdentityMismatch, + ); + let evidence_error = OfflineReplayVerificationError::Evidence( + CaptureManifestVerificationError::IdentityMismatch, + ); + + assert_eq!( + manifest_error.to_string(), + "offline replay persisted manifest bytes failed verification: capture manifest identity does not match" + ); + assert_eq!( + evidence_error.to_string(), + "offline replay capture evidence failed verification: capture manifest identity does not match" + ); + assert_eq!( + std::error::Error::source(&manifest_error).map(ToString::to_string), + Some("capture manifest identity does not match".to_owned()) + ); + assert_eq!( + std::error::Error::source(&evidence_error).map(ToString::to_string), + Some("capture manifest identity does not match".to_owned()) + ); +} From ff1aded4ad9467b9bfc465a1cea92cb4865597a4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 03:06:30 -0700 Subject: [PATCH 6/6] test(evidence): reject offline replay with missing WARC evidence --- .../tests/offline_capture_replay.rs | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/crates/originweave-evidence/tests/offline_capture_replay.rs b/crates/originweave-evidence/tests/offline_capture_replay.rs index 6991c8d6..d432fcab 100644 --- a/crates/originweave-evidence/tests/offline_capture_replay.rs +++ b/crates/originweave-evidence/tests/offline_capture_replay.rs @@ -1,10 +1,11 @@ #![allow(clippy::expect_used)] use originweave_evidence::{ - CaptureManifest, CaptureManifestValueBinding, CaptureManifestVerificationError, - EvidenceSourceKind, ExtractionCardinality, ExtractionField, ExtractionSchema, - ExtractionSourceChannel, ExtractionValueType, OfflineReplayVerificationError, ProvenanceRecord, - VerificationResult, WarcProvBundle, WarcResourceRecord, verify_offline_capture_package, + CaptureManifest, CaptureManifestError, CaptureManifestValueBinding, + CaptureManifestVerificationError, EvidenceSourceKind, ExtractionCardinality, ExtractionField, + ExtractionSchema, ExtractionSourceChannel, ExtractionValueType, OfflineReplayVerificationError, + ProvenanceRecord, VerificationResult, WarcProvBundle, WarcResourceRecord, + verify_offline_capture_package, }; const SOURCE_HASH: &str = "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; @@ -137,6 +138,35 @@ fn offline_replay_rejects_structured_result_identity_drift() { ); } +#[test] +fn offline_replay_rejects_missing_warc_evidence() { + let schema = schema(); + let record = resource_record(); + let bundle = WarcProvBundle::new(&record, SOFTWARE_COMMIT_SHA).expect("PROV bundle"); + let value = + CaptureManifestValueBinding::new("title", VALUE_HASH, RECORD_ID).expect("value binding"); + let manifest = CaptureManifest::new_with_warc_values( + &schema, + &[(&record, &bundle)], + std::slice::from_ref(&value), + ) + .expect("manifest"); + let serialized_manifest = manifest.to_json(); + + assert_eq!( + verify_offline_capture_package( + &manifest, + serialized_manifest.as_bytes(), + &schema, + &[], + std::slice::from_ref(&value), + ), + Err(OfflineReplayVerificationError::Evidence( + CaptureManifestVerificationError::InvalidCandidate(CaptureManifestError::MissingRecord), + )) + ); +} + #[test] fn offline_replay_errors_preserve_typed_diagnostics_and_sources() { let manifest_error = OfflineReplayVerificationError::ManifestBytes(