1use crate::merkle::Hash;
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use uuid::Uuid;
7
8#[cfg(feature = "algoswitch")]
9use vex_algoswitch as algoswitch;
10
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
13#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
14pub enum AuditEventType {
15 AgentCreated,
16 AgentExecuted,
17 DebateStarted,
18 DebateRound,
19 DebateConcluded,
20 ConsensusReached,
21 ContextStored,
22 PaymentInitiated,
23 PaymentCompleted,
24 PolicyUpdate,
26 ModelUpgrade,
27 GenomeEvolved,
28 AnomalousBehavior,
29 HumanOverride,
30 #[serde(rename = "CHORA_GATE_DECISION")]
32 GateDecision,
33 #[serde(untagged)]
34 Custom(String),
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
39pub struct EvidenceCapsule {
40 pub capsule_id: String,
41 pub outcome: String, pub reason_code: String,
43 pub witness_receipt: String,
44 pub nonce: u64,
45 #[serde(skip_serializing_if = "Option::is_none")]
47 pub magpie_source: Option<String>,
48 pub gate_sensors: serde_json::Value,
49 pub reproducibility_context: serde_json::Value,
50 #[serde(skip_serializing_if = "Option::is_none")]
52 pub vep_blob: Option<Vec<u8>>,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
57#[serde(tag = "type", content = "id", rename_all = "lowercase")]
58pub enum ActorType {
59 Bot(Uuid),
61 Human(String),
63 System(String),
65}
66
67impl Default for ActorType {
68 fn default() -> Self {
69 ActorType::System("vex_core".to_string())
70 }
71}
72
73impl ActorType {
74 pub fn pseudonymize(&self) -> Self {
76 match self {
77 Self::Human(id) => {
78 use sha2::{Digest, Sha256};
79 let mut hasher = Sha256::new();
80 hasher.update(id.as_bytes());
81 Self::Human(hex::encode(hasher.finalize()))
82 }
83 other => other.clone(),
84 }
85 }
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
90pub struct Signature {
91 pub signer_id: String,
92 pub signed_at: DateTime<Utc>,
93 pub signature_hex: String,
94}
95
96impl Signature {
97 pub fn create(
98 signer_id: impl Into<String>,
99 message: &[u8],
100 signing_key: &ed25519_dalek::SigningKey,
101 ) -> Self {
102 use ed25519_dalek::Signer;
103 let signature = signing_key.sign(message);
104
105 Self {
106 signer_id: signer_id.into(),
107 signed_at: Utc::now(),
108 signature_hex: hex::encode(signature.to_bytes()),
109 }
110 }
111
112 pub fn verify(
113 &self,
114 message: &[u8],
115 verifying_key: &ed25519_dalek::VerifyingKey,
116 ) -> Result<bool, String> {
117 let sig_bytes = match hex::decode(&self.signature_hex) {
118 Ok(bytes) => bytes,
119 Err(_) => return Ok(false),
120 };
121
122 let sig_array: [u8; 64] = match sig_bytes.try_into() {
123 Ok(arr) => arr,
124 Err(_) => return Ok(false),
125 };
126
127 let signature = ed25519_dalek::Signature::from_bytes(&sig_array);
128
129 match verifying_key.verify_strict(message, &signature) {
130 Ok(()) => Ok(true),
131 Err(e) => Err(format!("Signature verification failed: {}", e)),
132 }
133 }
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct AuditEvent {
139 pub id: Uuid,
140 pub event_type: AuditEventType,
141 pub timestamp: DateTime<Utc>,
142 pub agent_id: Option<Uuid>,
143 pub data: serde_json::Value,
144 pub hash: Hash,
145 pub previous_hash: Option<Hash>,
146 pub sequence_number: u64,
147
148 pub actor: ActorType,
150 pub rationale: Option<String>,
151 pub policy_version: Option<String>,
152 pub data_provenance_hash: Option<Hash>,
153 pub human_review_required: bool,
154 pub approval_signatures: Vec<Signature>,
155
156 #[serde(skip_serializing_if = "Option::is_none")]
158 pub evidence_capsule: Option<EvidenceCapsule>,
159 #[serde(skip_serializing_if = "Option::is_none")]
161 pub vep_blob: Option<Vec<u8>>,
162 pub schema_version: String,
163}
164
165#[derive(Serialize)]
167pub struct HashParams<'a> {
168 pub event_type: &'a AuditEventType,
169 pub timestamp: i64, pub sequence_number: u64,
171 pub data: &'a serde_json::Value,
172 pub actor: &'a ActorType,
173 #[serde(skip_serializing_if = "Option::is_none")]
174 pub rationale: &'a Option<String>,
175 #[serde(skip_serializing_if = "Option::is_none")]
176 pub policy_version: &'a Option<String>,
177 #[serde(skip_serializing_if = "Option::is_none")]
178 pub data_provenance_hash: &'a Option<Hash>,
179 pub human_review_required: bool,
180 pub approval_count: usize,
181 #[serde(skip_serializing_if = "Option::is_none")]
182 pub evidence_capsule: &'a Option<EvidenceCapsule>,
183 pub schema_version: &'a str,
184}
185
186impl AuditEvent {
187 const SENSITIVE_FIELDS: &'static [&'static str] = &[
189 "password",
190 "secret",
191 "token",
192 "api_key",
193 "apikey",
194 "key",
195 "authorization",
196 "auth",
197 "credential",
198 "private_key",
199 "privatekey",
200 ];
201
202 pub fn new(
204 event_type: AuditEventType,
205 agent_id: Option<Uuid>,
206 data: serde_json::Value,
207 sequence_number: u64,
208 ) -> Self {
209 let id = Uuid::new_v4();
210 let timestamp = Utc::now();
211
212 let data = Self::sanitize_data(data);
214
215 let actor = ActorType::System("vex_core".to_string());
217 let rationale: Option<String> = None;
218 let policy_version: Option<String> = None;
219 let data_provenance_hash: Option<Hash> = None;
220 let human_review_required = false;
221 let approval_signatures: Vec<Signature> = Vec::new();
222 let evidence_capsule: Option<EvidenceCapsule> = None;
223 let schema_version = "1.0".to_string();
224
225 let hash = Self::compute_hash(HashParams {
227 event_type: &event_type,
228 timestamp: timestamp.timestamp(),
229 sequence_number,
230 data: &data,
231 actor: &actor,
232 rationale: &rationale,
233 policy_version: &policy_version,
234 data_provenance_hash: &data_provenance_hash,
235 human_review_required,
236 approval_count: approval_signatures.len(),
237 evidence_capsule: &evidence_capsule,
238 schema_version: &schema_version,
239 });
240
241 Self {
242 id,
243 event_type,
244 timestamp,
245 agent_id,
246 data,
247 hash,
248 previous_hash: None,
249 sequence_number,
250 actor,
251 rationale,
252 policy_version,
253 data_provenance_hash,
254 human_review_required,
255 approval_signatures,
256 evidence_capsule,
257 vep_blob: None,
258 schema_version,
259 }
260 }
261
262 pub fn sanitize_data(value: serde_json::Value) -> serde_json::Value {
264 match value {
265 serde_json::Value::Object(mut map) => {
266 for key in map.keys().cloned().collect::<Vec<_>>() {
267 let lower_key = key.to_lowercase();
268 if Self::SENSITIVE_FIELDS.iter().any(|f| lower_key.contains(f)) {
269 map.insert(key, serde_json::Value::String("[REDACTED]".to_string()));
270 } else if let Some(v) = map.remove(&key) {
271 map.insert(key, Self::sanitize_data(v));
272 }
273 }
274 serde_json::Value::Object(map)
275 }
276 serde_json::Value::Array(arr) => {
277 serde_json::Value::Array(arr.into_iter().map(Self::sanitize_data).collect())
278 }
279 other => other,
280 }
281 }
282
283 pub fn chained(
285 event_type: AuditEventType,
286 agent_id: Option<Uuid>,
287 data: serde_json::Value,
288 previous_hash: Hash,
289 sequence_number: u64,
290 ) -> Self {
291 let mut event = Self::new(event_type, agent_id, data, sequence_number);
292 event.previous_hash = Some(previous_hash.clone());
293 event.hash = Self::compute_chained_hash(&event.hash, &previous_hash, sequence_number);
295 event
296 }
297
298 pub fn compute_hash(params: HashParams) -> Hash {
300 match serde_jcs::to_vec(¶ms) {
301 Ok(jcs_bytes) => Hash::digest(&jcs_bytes),
302 Err(_) => {
303 let content = format!(
305 "{:?}:{}:{}:{:?}:{:?}:{:?}:{:?}:{:?}:{}:{}:{}",
306 params.event_type,
307 params.timestamp,
308 params.sequence_number,
309 params.data,
310 params.actor,
311 params.rationale,
312 params.policy_version,
313 params.data_provenance_hash.as_ref().map(|h| h.to_hex()),
314 params.human_review_required,
315 params.approval_count,
316 params.schema_version,
317 );
318 Hash::digest(content.as_bytes())
319 }
320 }
321 }
322
323 pub fn compute_chained_hash(base_hash: &Hash, prev_hash: &Hash, sequence: u64) -> Hash {
324 let content = format!("{}:{}:{}", base_hash, prev_hash, sequence);
325 Hash::digest(content.as_bytes())
326 }
327
328 #[cfg(feature = "algoswitch")]
330 pub fn compute_optimized_hash(params: HashParams) -> u64 {
331 match serde_jcs::to_vec(¶ms) {
332 Ok(jcs_bytes) => algoswitch::select_hash(&jcs_bytes).0,
333 Err(_) => {
334 let content = format!(
335 "{:?}:{}:{}:{:?}:{:?}:{:?}:{}",
336 params.event_type,
337 params.timestamp,
338 params.sequence_number,
339 params.data,
340 params.actor,
341 params.rationale,
342 params.approval_count,
343 );
344 algoswitch::select_hash(content.as_bytes()).0
345 }
346 }
347 }
348
349 pub async fn sign_hardware(
352 &mut self,
353 agent_identity: &vex_hardware::api::AgentIdentity,
354 ) -> Result<(), String> {
355 let params = HashParams {
356 event_type: &self.event_type,
357 timestamp: self.timestamp.timestamp(),
358 sequence_number: self.sequence_number,
359 data: &self.data,
360 actor: &self.actor,
361 rationale: &self.rationale,
362 policy_version: &self.policy_version,
363 data_provenance_hash: &self.data_provenance_hash,
364 human_review_required: self.human_review_required,
365 approval_count: self.approval_signatures.len(),
366 evidence_capsule: &self.evidence_capsule,
367 schema_version: &self.schema_version,
368 };
369
370 let jcs_bytes =
372 serde_jcs::to_vec(¶ms).map_err(|e| format!("JCS serialization failed: {}", e))?;
373
374 let raw_signature_bytes = agent_identity.sign(&jcs_bytes);
376
377 let final_signature = Signature {
379 signer_id: agent_identity.agent_id.clone(),
380 signed_at: Utc::now(),
381 signature_hex: hex::encode(raw_signature_bytes),
382 };
383
384 self.approval_signatures.push(final_signature);
385 Ok(())
386 }
387}
388
389#[cfg(test)]
390mod tests {
391 use super::*;
392 use serde_json::json;
393
394 #[tokio::test]
395 async fn test_hardware_signature() {
396 std::env::set_var("VEX_HARDWARE_ATTESTATION", "false");
398
399 let keystore = vex_hardware::api::HardwareKeystore::new()
401 .await
402 .expect("Failed to initialize keystore");
403
404 let dummy_seed = [42u8; 32];
407 let encrypted_blob = keystore
408 .seal_identity(&dummy_seed)
409 .await
410 .expect("Failed to seal");
411 let agent_identity = keystore
412 .get_identity(&encrypted_blob)
413 .await
414 .expect("Failed to get identity");
415
416 let mut event = AuditEvent::new(
418 AuditEventType::GateDecision,
419 Some(Uuid::new_v4()),
420 json!({"status": "approved"}),
421 1,
422 );
423
424 assert!(
426 event.approval_signatures.is_empty(),
427 "Event should start with no signatures"
428 );
429
430 let sign_result = event.sign_hardware(&agent_identity).await;
431 assert!(sign_result.is_ok(), "Signing should succeed");
432
433 assert_eq!(
435 event.approval_signatures.len(),
436 1,
437 "One signature should be appended"
438 );
439
440 let sig = &event.approval_signatures[0];
441 assert_eq!(
442 sig.signer_id, agent_identity.agent_id,
443 "Signer ID should match agent identity"
444 );
445 assert!(
446 !sig.signature_hex.is_empty(),
447 "Signature hex should not be empty"
448 );
449 }
450
451 #[tokio::test]
452 async fn test_hardware_signature_deterministic() {
453 std::env::set_var("VEX_HARDWARE_ATTESTATION", "false");
454 let keystore = vex_hardware::api::HardwareKeystore::new().await.unwrap();
455 let dummy_seed = [42u8; 32];
456 let encrypted_blob = keystore.seal_identity(&dummy_seed).await.unwrap();
457 let agent_identity = keystore.get_identity(&encrypted_blob).await.unwrap();
458
459 let mut event1 = AuditEvent::new(
460 AuditEventType::GateDecision,
461 Some(Uuid::new_v4()),
462 json!({"status": "approved"}),
463 1,
464 );
465 let id = Uuid::new_v4();
467 let ts = Utc::now();
468 event1.id = id;
469 event1.timestamp = ts;
470
471 let mut event2 = event1.clone();
472
473 event1.sign_hardware(&agent_identity).await.unwrap();
474 event2.sign_hardware(&agent_identity).await.unwrap();
475
476 assert_eq!(
477 event1.approval_signatures[0].signature_hex,
478 event2.approval_signatures[0].signature_hex,
479 "Signatures for identical payloads must be deterministically equal"
480 );
481 }
482
483 #[tokio::test]
484 async fn test_hardware_signature_tamper_evident() {
485 std::env::set_var("VEX_HARDWARE_ATTESTATION", "false");
486 let keystore = vex_hardware::api::HardwareKeystore::new().await.unwrap();
487 let dummy_seed = [42u8; 32];
488 let encrypted_blob = keystore.seal_identity(&dummy_seed).await.unwrap();
489 let agent_identity = keystore.get_identity(&encrypted_blob).await.unwrap();
490
491 let mut event1 = AuditEvent::new(
492 AuditEventType::GateDecision,
493 Some(Uuid::new_v4()),
494 json!({"status": "approved"}),
495 1,
496 );
497 let mut event2 = event1.clone();
498
499 event2.data = json!({"status": "denied"});
501
502 event1.sign_hardware(&agent_identity).await.unwrap();
503 event2.sign_hardware(&agent_identity).await.unwrap();
504
505 assert_ne!(
506 event1.approval_signatures[0].signature_hex,
507 event2.approval_signatures[0].signature_hex,
508 "Signatures must change completely if the payload is tampered with"
509 );
510 }
511
512 #[tokio::test]
513 async fn test_hardware_signature_raw_dalek_verification() {
514 use ed25519_dalek::{Signature as DalekSignature, Verifier};
515
516 std::env::set_var("VEX_HARDWARE_ATTESTATION", "false");
517 let keystore = vex_hardware::api::HardwareKeystore::new().await.unwrap();
518 let dummy_seed = [42u8; 32];
519 let encrypted_blob = keystore.seal_identity(&dummy_seed).await.unwrap();
520 let agent_identity = keystore.get_identity(&encrypted_blob).await.unwrap();
521
522 let mut event = AuditEvent::new(
523 AuditEventType::GateDecision,
524 Some(Uuid::new_v4()),
525 json!({"status": "approved"}),
526 1,
527 );
528 event.sign_hardware(&agent_identity).await.unwrap();
529
530 let sig_hex = &event.approval_signatures[0].signature_hex;
531 let sig_bytes = hex::decode(sig_hex).unwrap();
532 let sig_array: [u8; 64] = sig_bytes.try_into().unwrap();
533 let dalek_sig = DalekSignature::from_bytes(&sig_array);
534
535 let params = HashParams {
537 event_type: &event.event_type,
538 timestamp: event.timestamp.timestamp(),
539 sequence_number: event.sequence_number,
540 data: &event.data,
541 actor: &event.actor,
542 rationale: &event.rationale,
543 policy_version: &event.policy_version,
544 data_provenance_hash: &event.data_provenance_hash,
545 human_review_required: event.human_review_required,
546 approval_count: 0, evidence_capsule: &event.evidence_capsule,
548 schema_version: &event.schema_version,
549 };
550 let expected_jcs_bytes = serde_jcs::to_vec(¶ms).unwrap();
551
552 let signing_key = ed25519_dalek::SigningKey::from_bytes(&dummy_seed);
556 let verifying_key = signing_key.verifying_key();
557
558 assert!(
560 verifying_key.verify(&expected_jcs_bytes, &dalek_sig).is_ok(),
561 "Raw Dalek verification failed: The generated signature does not mathematically match the JCS payload."
562 );
563 }
564}