Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
291b55c
test(evidence): require WARC PROV JSON-LD bundle
seonghobae Aug 22, 2026
e34b44a
feat(evidence): add bounded WARC PROV bundle
seonghobae Aug 22, 2026
fced5ba
feat(evidence): expose WARC PROV bundle
seonghobae Aug 22, 2026
d1003d2
test(evidence): cover provenance debug redaction
seonghobae Aug 22, 2026
a59e28f
test(evidence): preserve WARC completeness in PROV
seonghobae Aug 22, 2026
fc4e916
fix(evidence): retain WARC completeness in PROV
seonghobae Aug 22, 2026
39b29e2
test(evidence): pin completeness-aware PROV output
seonghobae Aug 22, 2026
93b2497
style(evidence): apply canonical Rust formatting
seonghobae Aug 22, 2026
008f526
style(evidence): format completeness regression
seonghobae Aug 22, 2026
c9130a2
docs(evidence): record completeness-aware PROV bundle
seonghobae Aug 22, 2026
e22ced7
docs(evidence): doctor PROV completeness semantics
seonghobae Aug 22, 2026
f9a1efb
test(evidence): prove PROV binds exact WARC record
seonghobae Aug 22, 2026
18a3096
fix(evidence): bind PROV to serialized WARC record
seonghobae Aug 22, 2026
72a91bb
test(evidence): pin serialized WARC digest in PROV
seonghobae Aug 22, 2026
d02ed44
test(evidence): require offline WARC PROV verification
seonghobae Aug 22, 2026
2102cf3
feat(evidence): verify WARC PROV bindings offline
seonghobae Aug 22, 2026
8d158b7
feat(evidence): export offline WARC PROV verification error
seonghobae Aug 22, 2026
080d002
docs(changelog): record offline WARC PROV verification
seonghobae Aug 22, 2026
22dbd91
test(evidence): require exact WARC provenance binding
seonghobae Aug 22, 2026
a6e61c1
fix(evidence): bind offline PROV verification to exact source evidence
seonghobae Aug 22, 2026
3af43d9
style(evidence): apply canonical rustfmt
seonghobae Aug 22, 2026
0f781cd
test(evidence): keep provenance regression clippy-clean
seonghobae Aug 22, 2026
e41bf2c
test(prov): reject null software revision identity
seonghobae Aug 22, 2026
133289e
fix(prov): reject null Git software identity
seonghobae Aug 22, 2026
bb7fb76
Merge updated WARC identity prerequisite
seonghobae Aug 23, 2026
56fcfa5
chore(evidence): realign PROV stack to WARC parent
seonghobae Aug 23, 2026
003a83d
chore(evidence): realign PROV bundle to current WARC prerequisite
seonghobae Aug 25, 2026
c11e733
feat(evidence): expose PROV bundle on current stack
seonghobae Aug 25, 2026
4647915
chore(evidence): realign PROV bundle with WARC parent
seonghobae Aug 27, 2026
85eef87
docs(evidence): document WARC PROV bundle boundary
seonghobae Aug 28, 2026
33e6c6e
docs(evidence): trace WARC PROV bundle boundary
seonghobae Aug 28, 2026
f1fcdd0
refactor(evidence): share WARC truncation tokens
seonghobae Aug 28, 2026
6d5851b
merge: sync WARC provenance base
seonghobae Aug 28, 2026
1d5d293
fix: make WARC provenance digest binding authoritative
seonghobae Aug 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ All notable changes to OriginWeave are documented in this file. The format follo
- Authoritative product documentation graph spanning PRD, TRD, ADR lifecycle/index, product-wide UML, conceptual ERD, requirement/decision traceability, threat modeling, product-wide test strategy, operability, API/protocol, release/rollback, and current primary-source standards doctoring, with machine-checkable repository contracts that keep conversation-derived future work distinct from protected-main implementation claims.
- Purpose-bound data-governance and privacy baseline that rejects both blanket masking and ambient raw-value propagation, defines field-scoped just-in-time disclosure, opaque-handle/trusted-broker boundaries, model/provider/region policy, retention/deletion/residency/break-glass controls, truthful CSAP/SOC 2 readiness language, and machine-checkable documentation contracts without inventing an OriginWeave-owned production database.
- Proposed product-wide target-architecture ADRs for the Rust control plane, isolated execution modes, typed actions, semantic observation/stale-node authority, prompt-injection and secret separation, resource-governor priority, provenance evidence, browser/protocol adapters, crawler policy, and hourly automation operational closure; these remain Proposed rather than shipped claims until protected review and merge.
- Active PR #217 adds a bounded in-memory `WarcProvBundle` for deterministic W3C PROV-O JSON-LD projection and offline verification of exact WARC records; durable persistence, retention, and transport-specific export remain planned rather than shipped.
- Connected the active WARC/PROV bundle evidence to the primary standards doctoring record, preserving the in-memory-only and non-shipped durable-adapter boundary.
- Made WARC/PROV offline verification use the complete deterministic WARC digest as the authoritative block-digest binding, preserving fail-closed exact-record checks while keeping the public verifier surface minimal.

### Changed

Expand Down
5 changes: 5 additions & 0 deletions crates/originweave-evidence/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
mod extraction_schema;
mod sensitive_access;
mod sensitive_handle_lifecycle;
mod warc_prov_bundle;
mod warc_resource_record;

pub use extraction_schema::{
Expand All @@ -25,6 +26,10 @@ pub use sensitive_access::{
pub use sensitive_handle_lifecycle::{
SensitiveHandleLifecycleEvidence, SensitiveHandleLifecycleEvidenceInput,
};
pub use warc_prov_bundle::{
MAX_PROV_SOFTWARE_COMMIT_SHA_BYTES, WarcProvBundle, WarcProvBundleError,
WarcProvBundleVerificationError,
};
Comment thread
seonghobae marked this conversation as resolved.
pub use warc_resource_record::{
MAX_WARC_CONTENT_TYPE_BYTES, MAX_WARC_DATE_BYTES, MAX_WARC_PAYLOAD_BYTES,
MAX_WARC_RECORD_ID_BYTES, MAX_WARC_TARGET_URI_BYTES, WarcPayloadCompleteness,
Expand Down
273 changes: 273 additions & 0 deletions crates/originweave-evidence/src/warc_prov_bundle.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
use std::fmt;

use sha2::{Digest, Sha256};

use crate::{ProvenanceRecord, WarcPayloadCompleteness, WarcResourceRecord};

const ORIGINWEAVE_COMMIT_URL_PREFIX: &str =
"https://github.com/ContextualWisdomLab/OriginWeave/commit/";
const WARC_RECORD_DIGEST_IRI: &str =
"tag:contextualwisdomlab.github.io,2026:OriginWeave/warcRecordDigest";
const WARC_PAYLOAD_COMPLETENESS_IRI: &str =
"tag:contextualwisdomlab.github.io,2026:OriginWeave/warcPayloadCompleteness";
const WARC_TRUNCATION_REASON_IRI: &str =
"tag:contextualwisdomlab.github.io,2026:OriginWeave/warcTruncationReason";

/// Exact byte length accepted for a canonical Git SHA-1 software revision.
pub const MAX_PROV_SOFTWARE_COMMIT_SHA_BYTES: usize = 40;

/// A validation failure while constructing a deterministic WARC provenance bundle.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WarcProvBundleError {
/// The software revision was not one canonical lower-case 40-byte Git SHA-1.
InvalidSoftwareCommitSha,
/// A bounded provenance field exceeded its allowed size.
LimitExceeded,
}

impl fmt::Display for WarcProvBundleError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::InvalidSoftwareCommitSha => "invalid OriginWeave software commit SHA",
Self::LimitExceeded => "WARC PROV bundle limit exceeded",
})
}
}

impl std::error::Error for WarcProvBundleError {}

/// A deterministic offline verification failure between a PROV bundle and a WARC record.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WarcProvBundleVerificationError {
/// The WARC record identifier differs from the identifier bound into the PROV bundle.
RecordIdentityMismatch,
/// The independently verified source provenance differs from the provenance bound into the bundle.
SourceEvidenceMismatch,
/// The WARC capture timestamp differs from the timestamp bound into the PROV bundle.
CaptureTimeMismatch,
/// The WARC complete-versus-truncated state differs from the state bound into the bundle.
PayloadCompletenessMismatch,
/// The digest of the deterministic WARC serialization differs from the bundle binding.
WarcRecordDigestMismatch,
}

impl fmt::Display for WarcProvBundleVerificationError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::RecordIdentityMismatch => "WARC PROV record identity does not match",
Self::SourceEvidenceMismatch => "WARC PROV source evidence does not match",
Self::CaptureTimeMismatch => "WARC PROV capture time does not match",
Self::PayloadCompletenessMismatch => "WARC PROV payload completeness does not match",
Self::WarcRecordDigestMismatch => "WARC PROV serialized record digest does not match",
})
}
}

impl std::error::Error for WarcProvBundleVerificationError {}

/// A deterministic PROV-O JSON-LD projection over one validated WARC resource record.
///
/// The bundle contains identifiers, exact validated source provenance, record hashes, source
/// location, capture time, explicit WARC payload completeness, and the exact OriginWeave software
/// revision. It deliberately does not retain or emit the WARC payload.
#[derive(Clone, PartialEq, Eq)]
pub struct WarcProvBundle {
record_entity_id: String,
source_entity_id: String,
capture_activity_id: String,
software_agent_id: String,
software_commit_sha: String,
source_provenance: ProvenanceRecord,
warc_date: String,
block_digest: String,
warc_record_digest: String,
payload_completeness: WarcPayloadCompleteness,
}

impl fmt::Debug for WarcProvBundle {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("WarcProvBundle")
.field("record_entity_id", &self.record_entity_id)
.field("source_entity_id", &self.source_entity_id)
.field("capture_activity_id", &self.capture_activity_id)
.field("software_agent_id", &self.software_agent_id)
.field("payload_completeness", &self.payload_completeness)
.finish_non_exhaustive()
}
}

impl WarcProvBundle {
/// Construct a provenance bundle from one already-validated WARC resource record.
///
/// `software_commit_sha` is an immutable canonical Git SHA-1 identifier. This constructor
/// does not contact GitHub and does not treat the identifier as authentication or authority.
pub fn new(
record: &WarcResourceRecord,
software_commit_sha: &str,
) -> Result<Self, WarcProvBundleError> {
if software_commit_sha.len() > MAX_PROV_SOFTWARE_COMMIT_SHA_BYTES {
return Err(WarcProvBundleError::LimitExceeded);
}
if !valid_software_commit_sha(software_commit_sha) {
return Err(WarcProvBundleError::InvalidSoftwareCommitSha);
}

let record_entity_id = record.record_id().to_owned();
let source_entity_id = format!("{}#source", record.record_id());
let capture_activity_id = format!("{}#capture", record.record_id());
let software_agent_id = format!("{ORIGINWEAVE_COMMIT_URL_PREFIX}{software_commit_sha}");
let warc_record_digest = sha256_digest(&record.to_warc_bytes());

Ok(Self {
record_entity_id,
source_entity_id,
capture_activity_id,
software_agent_id,
software_commit_sha: software_commit_sha.to_owned(),
source_provenance: record.provenance().clone(),
warc_date: record.warc_date().to_owned(),
block_digest: record.block_digest().to_owned(),
warc_record_digest,
payload_completeness: record.completeness(),
})
}

/// Return the PROV entity identifier for the WARC record.
#[must_use]
pub fn record_entity_id(&self) -> &str {
&self.record_entity_id
}

/// Return the PROV entity identifier for the independently verified source.
#[must_use]
pub fn source_entity_id(&self) -> &str {
&self.source_entity_id
}

/// Return the PROV activity identifier for this capture.
#[must_use]
pub fn capture_activity_id(&self) -> &str {
&self.capture_activity_id
}

/// Return the immutable OriginWeave commit URL used as the PROV software-agent identifier.
#[must_use]
pub fn software_agent_id(&self) -> &str {
&self.software_agent_id
}

/// Return the canonical lower-case Git SHA-1 of the OriginWeave revision.
#[must_use]
pub fn software_commit_sha(&self) -> &str {
&self.software_commit_sha
}

/// Verify offline that one validated WARC record is exactly the record bound by this bundle.
///
/// Verification includes the complete validated [`ProvenanceRecord`] rather than only the
/// source URL and digest, so locator or evidence-channel drift cannot collapse into a match.
/// It is deterministic and performs no network, DNS, browser, model, persistence, or authority
/// operation. A matching digest proves byte identity only; it does not authenticate the actor
/// that produced either value or establish factual correctness.
pub fn verify_record(
&self,
record: &WarcResourceRecord,
) -> Result<(), WarcProvBundleVerificationError> {
if self.record_entity_id != record.record_id() {
return Err(WarcProvBundleVerificationError::RecordIdentityMismatch);
}
if self.source_provenance != *record.provenance() {
return Err(WarcProvBundleVerificationError::SourceEvidenceMismatch);
}
if self.warc_date != record.warc_date() {
return Err(WarcProvBundleVerificationError::CaptureTimeMismatch);
}
// `warc_record_digest` covers the complete deterministic serialization, including the
// WARC-Block-Digest header, so a block-digest drift is reported by that binding below.
if self.payload_completeness != record.completeness() {
return Err(WarcProvBundleVerificationError::PayloadCompletenessMismatch);
}
if self.warc_record_digest != sha256_digest(&record.to_warc_bytes()) {
return Err(WarcProvBundleVerificationError::WarcRecordDigestMismatch);
}
Comment thread
seonghobae marked this conversation as resolved.
Ok(())
}
Comment thread
seonghobae marked this conversation as resolved.

/// Serialize the bundle as deterministic compact W3C PROV-O JSON-LD.
///
/// All interpolated values originate from the validated WARC record or the canonical
/// lower-case software commit identifier, so no raw payload bytes enter this document. The
/// payload block digest binds the retained resource bytes while `warcRecordDigest` binds the
/// complete deterministic WARC serialization, including its headers. WARC payload completeness
/// is retained as an OriginWeave-owned absolute-IRI attribute; truncated records also retain
/// the exact WARC truncation token. The JSON-LD projection exposes source URL and digest while
/// offline verification additionally preserves the exact validated source locator and channel.
#[must_use]
pub fn to_json_ld(&self) -> String {
let completeness_attributes =
warc_payload_completeness_attributes(self.payload_completeness);
format!(
"{{\"@context\":{{\"prov\":\"http://www.w3.org/ns/prov#\",\"xsd\":\"http://www.w3.org/2001/XMLSchema#\"}},\"@graph\":[{{\"@id\":\"{}\",\"@type\":\"prov:Entity\",\"prov:atLocation\":{{\"@id\":\"{}\"}},\"prov:value\":\"{}\"}},{{\"@id\":\"{}\",\"@type\":\"prov:Activity\",\"prov:startedAtTime\":{{\"@value\":\"{}\",\"@type\":\"xsd:dateTime\"}},\"prov:used\":{{\"@id\":\"{}\"}},\"prov:wasAssociatedWith\":{{\"@id\":\"{}\"}}}},{{\"@id\":\"{}\",\"@type\":\"prov:SoftwareAgent\"}},{{\"@id\":\"{}\",\"@type\":\"prov:Entity\",\"prov:value\":\"{}\",\"{WARC_RECORD_DIGEST_IRI}\":\"{}\",{},\"prov:wasDerivedFrom\":{{\"@id\":\"{}\"}},\"prov:wasGeneratedBy\":{{\"@id\":\"{}\"}}}}]}}",
self.source_entity_id,
self.source_provenance.source_url(),
self.source_provenance.source_hash(),
self.capture_activity_id,
self.warc_date,
self.source_entity_id,
self.software_agent_id,
self.software_agent_id,
self.record_entity_id,
self.block_digest,
self.warc_record_digest,
completeness_attributes,
self.source_entity_id,
self.capture_activity_id,
)
}
Comment thread
seonghobae marked this conversation as resolved.
}

fn warc_payload_completeness_attributes(completeness: WarcPayloadCompleteness) -> String {
match completeness {
WarcPayloadCompleteness::Complete => {
format!("\"{WARC_PAYLOAD_COMPLETENESS_IRI}\":\"complete\"")
}
WarcPayloadCompleteness::Truncated(reason) => format!(
"\"{WARC_PAYLOAD_COMPLETENESS_IRI}\":\"truncated\",\"{WARC_TRUNCATION_REASON_IRI}\":\"{}\"",
reason.warc_token()
),
}
}

fn sha256_digest(bytes: &[u8]) -> String {
let digest = Sha256::digest(bytes);
let mut encoded = String::from("sha256:");
for byte in digest {
encoded.push_str(&format!("{byte:02x}"));
}
encoded
}

fn valid_software_commit_sha(software_commit_sha: &str) -> bool {
software_commit_sha.len() == MAX_PROV_SOFTWARE_COMMIT_SHA_BYTES
&& software_commit_sha
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
&& software_commit_sha.bytes().any(|byte| byte != b'0')
Comment thread
seonghobae marked this conversation as resolved.
}

#[cfg(test)]
mod tests {
use crate::WarcTruncationReason;

#[test]
fn truncation_tokens_use_the_shared_warc_mapping() {
assert_eq!(WarcTruncationReason::Length.warc_token(), "length");
assert_eq!(WarcTruncationReason::Time.warc_token(), "time");
assert_eq!(WarcTruncationReason::Disconnect.warc_token(), "disconnect");
assert_eq!(
WarcTruncationReason::Unspecified.warc_token(),
"unspecified"
);
}
}
38 changes: 38 additions & 0 deletions crates/originweave-evidence/src/warc_resource_record.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ pub enum WarcTruncationReason {
}

impl WarcTruncationReason {
/// Return the canonical WARC 1.1 truncation token for this reason.
pub(crate) const fn warc_token(self) -> &'static str {
match self {
Self::Length => "length",
Expand Down Expand Up @@ -522,3 +523,40 @@ fn sha256_digest(payload: &[u8]) -> String {
}
encoded
}

#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
use super::*;
use crate::{WarcProvBundle, WarcProvBundleVerificationError};

#[test]
fn provenance_bundle_rejects_a_tampered_block_digest() {
let provenance = ProvenanceRecord::new(
"https://example.com/item",
"body",
"sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
crate::EvidenceSourceKind::NetworkResponse,
crate::VerificationResult::Verified,
)
.expect("verified provenance");
let mut record = WarcResourceRecord::new(
"urn:uuid:123e4567-e89b-12d3-a456-426614174000",
"2026-08-22T12:00:00Z",
"https://example.com/item",
"text/plain",
b"hello".to_vec(),
provenance,
)
.expect("WARC resource record");
let bundle = WarcProvBundle::new(&record, "0123456789abcdef0123456789abcdef01234567")
.expect("PROV bundle");

record.block_digest = "sha256:tampered".to_owned();

assert_eq!(
bundle.verify_record(&record),
Err(WarcProvBundleVerificationError::WarcRecordDigestMismatch)
);
}
}
Loading
Loading