diff --git a/crates/vex-chora/src/bridge.rs b/crates/vex-chora/src/bridge.rs index c89cfa1..56a16bd 100644 --- a/crates/vex-chora/src/bridge.rs +++ b/crates/vex-chora/src/bridge.rs @@ -57,11 +57,12 @@ impl AuthorityBridge { }; // 4. WitnessData — populated from the live CHORA response. - let now = chrono::Utc::now().to_rfc3339(); + let now = chrono::Utc::now().timestamp() as u64; let witness = WitnessData { chora_node_id: response.authority.capsule_id.clone(), receipt_hash: response.signature.clone(), timestamp: now, + metadata: serde_json::Value::Null, }; // 5. Build Pillar Hashes @@ -78,7 +79,7 @@ impl AuthorityBridge { let authority_hash = hash_seg(&response.authority)?; let identity_hash = hash_seg(&identity)?; - let witness_hash = hash_seg(&witness)?; + let witness_hash = witness.to_commitment_hash()?; // 6. Build Composite Capsule let mut capsule = Capsule { diff --git a/crates/vex-core/src/segment.rs b/crates/vex-core/src/segment.rs index dd38627..8dd0df2 100644 --- a/crates/vex-core/src/segment.rs +++ b/crates/vex-core/src/segment.rs @@ -54,7 +54,48 @@ fn default_sensor_value() -> serde_json::Value { pub struct WitnessData { pub chora_node_id: String, pub receipt_hash: String, - pub timestamp: String, + pub timestamp: u64, + /// Diagnostic or display-only fields that are NOT part of the commitment surface. + #[serde(flatten, default)] + pub metadata: serde_json::Value, +} + +/// The "Commitment Surface" for the Witness segment. +/// Lexicographical order and field presence are strictly enforced here for interop parity. +#[derive(Serialize)] +struct MinimalWitness<'a> { + chora_node_id: &'a str, + receipt_hash: &'a str, + timestamp: u64, +} + +impl WitnessData { + /// Compute the SHA-256 hash of the JCS-canonicalized MINIMAL witness structure. + /// This ensures that adding extra display fields to the JSON does not break the commitment. + pub fn to_commitment_hash(&self) -> Result { + let minimal = MinimalWitness { + chora_node_id: &self.chora_node_id, + receipt_hash: &self.receipt_hash, + timestamp: self.timestamp, + }; + + let jcs_bytes = serde_jcs::to_vec(&minimal) + .map_err(|e| format!("JCS serialization of minimal witness failed: {}", e))?; + + let mut hasher = Sha256::new(); + hasher.update(&jcs_bytes); + Ok(hex::encode(hasher.finalize())) + } + + pub fn to_jcs_hash(&self) -> Result { + let hex = self.to_commitment_hash()?; + Ok(Hash::from_bytes( + hex::decode(hex) + .map_err(|e| e.to_string())? + .try_into() + .map_err(|_| "Invalid hash length")?, + )) + } } /// Identity Data (Attest Pillar) @@ -132,7 +173,7 @@ impl Capsule { let authority_hash_hex = hash_seg(&self.authority)?; let identity_hash_hex = hash_seg(&self.identity)?; - let witness_hash_hex = hash_seg(&self.witness)?; + let witness_hash_hex = self.witness.to_commitment_hash()?; // Build the Canonical Composite Object let composite_root = serde_json::json!({ @@ -191,4 +232,31 @@ mod tests { assert_ne!(hash1, hash2, "Hashes must change when content changes"); } + + #[test] + fn test_witness_metadata_exclusion() { + let base_witness = WitnessData { + chora_node_id: "node-1".to_string(), + receipt_hash: "hash-1".to_string(), + timestamp: 1710396000, + metadata: serde_json::Value::Null, + }; + + let hash_base = base_witness.to_commitment_hash().unwrap(); + + let mut metadata_witness = base_witness.clone(); + metadata_witness.metadata = serde_json::json!({ + "witness_mode": "sentinel", + "diagnostics": { + "latency_ms": 42 + } + }); + + let hash_with_metadata = metadata_witness.to_commitment_hash().unwrap(); + + assert_eq!( + hash_base, hash_with_metadata, + "Witness hash must be invariant to extra metadata fields" + ); + } } diff --git a/crates/vex-core/src/vep.rs b/crates/vex-core/src/vep.rs index 07ef61e..0a48c69 100644 --- a/crates/vex-core/src/vep.rs +++ b/crates/vex-core/src/vep.rs @@ -1,4 +1,4 @@ -//! VEP (Viking Enveloped Packet) Binary Format +//! VEP (Verifiable Evidence Packet) Binary Format //! //! A zero-copy, high-performance binary envelope for segmented VEX audit data. @@ -174,7 +174,7 @@ impl<'a> VepPacket<'a> { let authority_hash = hash_seg(&authority)?; let identity_hash = hash_seg(&identity)?; - let witness_hash = hash_seg(&witness)?; + let witness_hash = witness.to_commitment_hash()?; let mut capsule = Capsule { capsule_id: authority.capsule_id.clone(), diff --git a/crates/vex-core/tests/chora_parity.rs b/crates/vex-core/tests/chora_parity.rs index 5c678a8..c81dba1 100644 --- a/crates/vex-core/tests/chora_parity.rs +++ b/crates/vex-core/tests/chora_parity.rs @@ -30,7 +30,8 @@ fn test_capsule_jcs_parity() { let witness = WitnessData { chora_node_id: "test-chora-node".into(), receipt_hash: "deadbeef".into(), - timestamp: "2024-03-09T10:00:00Z".into(), + timestamp: 1710396000, + metadata: serde_json::Value::Null, }; let capsule = Capsule { @@ -66,7 +67,7 @@ fn test_capsule_jcs_parity() { let auth_hash = hash_seg(&authority); let id_hash = hash_seg(&identity); - let wit_hash = hash_seg(&witness); + let wit_hash = witness.to_commitment_hash().unwrap(); println!("--- RUST NATIVE HASHES ---"); println!("Intent Hash: {}", intent_hash.to_hex()); diff --git a/crates/vex-runtime/src/audit/vep.rs b/crates/vex-runtime/src/audit/vep.rs index 3c28039..ae4891b 100644 --- a/crates/vex-runtime/src/audit/vep.rs +++ b/crates/vex-runtime/src/audit/vep.rs @@ -52,7 +52,28 @@ pub struct IdentitySegment { pub struct WitnessSegment { pub chora_node_id: String, pub receipt_hash: String, - pub timestamp: String, + pub timestamp: u64, + /// Diagnostic or display-only fields that are NOT part of the commitment surface. + #[serde(flatten, default)] + pub metadata: serde_json::Value, +} + +impl WitnessSegment { + /// Compute the SHA-256 hash of the JCS-canonicalized MINIMAL witness structure. + pub fn to_commitment_hash(&self) -> Result { + // Strict commitment to only the core fields (Unix timestamp handles JCS parity) + let minimal = serde_json::json!({ + "chora_node_id": self.chora_node_id, + "receipt_hash": self.receipt_hash, + "timestamp": self.timestamp, + }); + + let jcs_bytes = serde_jcs::to_vec(&minimal).map_err(|e| VepError::Jcs(e.to_string()))?; + + let mut hasher = Sha256::new(); + hasher.update(&jcs_bytes); + Ok(hex::encode(hasher.finalize())) + } } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -101,7 +122,7 @@ impl EvidenceCapsuleV0 { let intent_hash = hash_segment(&intent)?; let authority_hash = hash_segment(&authority)?; let identity_hash = hash_segment(&identity)?; - let witness_hash = hash_segment(&witness)?; + let witness_hash = witness.to_commitment_hash()?; // Root commitment: JCS lexicographical order is handled by serde_jcs/serde_json let root_map = serde_json::json!({ diff --git a/crates/vex-runtime/src/audit/verify.rs b/crates/vex-runtime/src/audit/verify.rs index 61d2df3..787be23 100644 --- a/crates/vex-runtime/src/audit/verify.rs +++ b/crates/vex-runtime/src/audit/verify.rs @@ -73,6 +73,7 @@ impl VepVerifier { chora_node_id: core_capsule.witness.chora_node_id, receipt_hash: core_capsule.witness.receipt_hash, timestamp: core_capsule.witness.timestamp, + metadata: core_capsule.witness.metadata, }; let request_commitment = core_capsule.request_commitment.map(|rc| RequestCommitment { @@ -168,7 +169,8 @@ mod tests { let witness = WitnessSegment { chora_node_id: "node1".to_string(), receipt_hash: "rh".to_string(), - timestamp: "now".to_string(), + timestamp: 1710403200, + metadata: serde_json::Value::Null, }; let mut capsule = @@ -230,7 +232,8 @@ mod tests { let witness = WitnessSegment { chora_node_id: "nodeB".to_string(), receipt_hash: "receiptB".to_string(), - timestamp: "2024-03-12T00:00:00Z".to_string(), + timestamp: 1710403200, + metadata: serde_json::Value::Null, }; let mut capsule = diff --git a/crates/vex-runtime/src/gate/titan.rs b/crates/vex-runtime/src/gate/titan.rs index bf6153d..f848ceb 100644 --- a/crates/vex-runtime/src/gate/titan.rs +++ b/crates/vex-runtime/src/gate/titan.rs @@ -349,7 +349,8 @@ impl Gate for TitanGate { let witness = WitnessSegment { chora_node_id: "chora-primary-v1".to_string(), receipt_hash: chora_resp.signature.clone(), - timestamp: chrono::Utc::now().to_rfc3339(), + timestamp: chrono::Utc::now().timestamp() as u64, + metadata: serde_json::Value::Null, }; let mut v0_capsule = match EvidenceCapsuleV0::new( diff --git a/crates/vex-runtime/tests/vep_verification.rs b/crates/vex-runtime/tests/vep_verification.rs index 386962f..7b3ecfc 100644 --- a/crates/vex-runtime/tests/vep_verification.rs +++ b/crates/vex-runtime/tests/vep_verification.rs @@ -30,7 +30,8 @@ fn test_vep_binary_serialization() { let witness = WitnessSegment { chora_node_id: "node-1".to_string(), receipt_hash: "2".repeat(64), - timestamp: "2026-03-11T22:00:00Z".to_string(), + timestamp: 1710403200, + metadata: serde_json::json!({}), }; let capsule = EvidenceCapsuleV0::new(intent, authority, identity, witness, None).unwrap(); @@ -111,7 +112,8 @@ fn test_vep_signature_verification() { WitnessSegment { chora_node_id: "n".into(), receipt_hash: "2".repeat(64), - timestamp: "2026".into(), + timestamp: 1710403200, + metadata: serde_json::json!({}), }, None, ) diff --git a/crates/vex-sidecar/src/main.rs b/crates/vex-sidecar/src/main.rs index c4926e0..cc1efde 100644 --- a/crates/vex-sidecar/src/main.rs +++ b/crates/vex-sidecar/src/main.rs @@ -105,7 +105,8 @@ async fn proxy_handler( let witness = WitnessData { chora_node_id: "sidecar-local".to_string(), receipt_hash: payload_hash.clone(), // In proxy mode, we link to payload - timestamp: chrono::Utc::now().to_rfc3339(), + timestamp: chrono::Utc::now().timestamp() as u64, + metadata: serde_json::Value::Null, }; // 5. Build Pillar Hashes @@ -118,7 +119,7 @@ async fn proxy_handler( let authority_hash = hash_seg(&authority); let identity_hash = hash_seg(&identity); - let witness_hash = hash_seg(&witness); + let witness_hash = witness.to_commitment_hash().unwrap(); // 6. Assemble Hardened Capsule let mut capsule = Capsule {