Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 3 additions & 2 deletions crates/vex-chora/src/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down
72 changes: 70 additions & 2 deletions crates/vex-core/src/segment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String> {
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<Hash, String> {
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)
Expand Down Expand Up @@ -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!({
Expand Down Expand Up @@ -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"
);
}
}
4 changes: 2 additions & 2 deletions crates/vex-core/src/vep.rs
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -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(),
Expand Down
5 changes: 3 additions & 2 deletions crates/vex-core/tests/chora_parity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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());
Expand Down
25 changes: 23 additions & 2 deletions crates/vex-runtime/src/audit/vep.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, VepError> {
// 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)]
Expand Down Expand Up @@ -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!({
Expand Down
7 changes: 5 additions & 2 deletions crates/vex-runtime/src/audit/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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 =
Expand Down
3 changes: 2 additions & 1 deletion crates/vex-runtime/src/gate/titan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
6 changes: 4 additions & 2 deletions crates/vex-runtime/tests/vep_verification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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,
)
Expand Down
5 changes: 3 additions & 2 deletions crates/vex-sidecar/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down
Loading