Skip to main content

vex_core/
audit.rs

1//! Audit log types with Merkle verification (ISO 42001 / EU AI Act compliant)
2
3use 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/// Audit event types
12#[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    // ISO 42001 A.6 Lifecycle event types
25    PolicyUpdate,
26    ModelUpgrade,
27    GenomeEvolved,
28    AnomalousBehavior,
29    HumanOverride,
30    /// CHORA Phase-2 Gate Decision
31    #[serde(rename = "CHORA_GATE_DECISION")]
32    GateDecision,
33    #[serde(untagged)]
34    Custom(String),
35}
36
37/// CHORA Evidence Capsule (RFC 8785 Compliant Metadata)
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
39pub struct EvidenceCapsule {
40    pub capsule_id: String,
41    pub outcome: String, // ALLOW, HALT, ESCALATE
42    pub reason_code: String,
43    pub witness_receipt: String,
44    pub nonce: u64,
45    /// Bundled Magpie AST for independent verification
46    #[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    /// Optional full VEP binary blob
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub vep_blob: Option<Vec<u8>>,
53}
54
55/// Actor type for audit attribution (ISO 42001 A.6.2.8)
56#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
57#[serde(tag = "type", content = "id", rename_all = "lowercase")]
58pub enum ActorType {
59    /// AI agent performed the action
60    Bot(Uuid),
61    /// Human user performed the action
62    Human(String),
63    /// System/automated process
64    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    /// Pseudonymize human actor ID using SHA-256 to protect PII (ISO 42001 A.6.2.8)
75    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/// Cryptographic signature for multi-party authorization (ISO 42001 A.6.1.3)
89#[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/// Single audit event (ISO 42001 / EU AI Act compliant)
137#[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    // Compliance Fields
149    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    // CHORA Alignment
157    #[serde(skip_serializing_if = "Option::is_none")]
158    pub evidence_capsule: Option<EvidenceCapsule>,
159    /// Optional full VEP binary blob for independent verification
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub vep_blob: Option<Vec<u8>>,
162    pub schema_version: String,
163}
164
165/// Parameters for consistent event hashing
166#[derive(Serialize)]
167pub struct HashParams<'a> {
168    pub event_type: &'a AuditEventType,
169    pub timestamp: i64, // Use timestamp for JCS stability
170    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    /// Fields that should be redacted from audit log data for security
188    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    /// Create a new audit event with sanitized data
203    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        // Sanitize sensitive fields from data
213        let data = Self::sanitize_data(data);
214
215        // Default ISO 42001 / EU AI Act fields
216        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        // Compute hash including ALL fields (Centralized in vex-core)
226        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    /// Sanitize sensitive fields from audit data (HIGH-2 fix)
263    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    /// Create with chained previous hash
284    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        // Rehash including previous hash and sequence (Centralized in vex-core)
294        event.hash = Self::compute_chained_hash(&event.hash, &previous_hash, sequence_number);
295        event
296    }
297
298    /// Hashing using RFC 8785 (JCS) for cross-platform determinism
299    pub fn compute_hash(params: HashParams) -> Hash {
300        match serde_jcs::to_vec(&params) {
301            Ok(jcs_bytes) => Hash::digest(&jcs_bytes),
302            Err(_) => {
303                // Fallback (should not happen if HashParams is simple)
304                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    /// Optimized hash computation using AlgoSwitch for non-critical performance tracing
329    #[cfg(feature = "algoswitch")]
330    pub fn compute_optimized_hash(params: HashParams) -> u64 {
331        match serde_jcs::to_vec(&params) {
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    /// Sign the evidence capsule payload using the hardware-rooted TPM identity.
350    /// Uses JCS serialization for deterministic CHORA compliance.
351    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        // 1. Serialize parameters to JCS bytes deterministically
371        let jcs_bytes =
372            serde_jcs::to_vec(&params).map_err(|e| format!("JCS serialization failed: {}", e))?;
373
374        // 2 & 3. Generate the signature directly over the JCS bytes using hardware-rooted identity
375        let raw_signature_bytes = agent_identity.sign(&jcs_bytes);
376
377        // 4. Wrap the raw signature in VEX's unified Signature tracking format
378        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        // Force fallback mode for the test (no physical TPM needed)
397        std::env::set_var("VEX_HARDWARE_ATTESTATION", "false");
398
399        // 1. Initialize the mock/fallback hardware keystore
400        let keystore = vex_hardware::api::HardwareKeystore::new()
401            .await
402            .expect("Failed to initialize keystore");
403
404        // 2. Generate a random identity
405        // In fallback mode, get_identity requires a seed blob. Let's create a dummy encrypted blob (fallback provider expects raw 32 bytes)
406        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        // 3. Create a sample audit event
417        let mut event = AuditEvent::new(
418            AuditEventType::GateDecision,
419            Some(Uuid::new_v4()),
420            json!({"status": "approved"}),
421            1,
422        );
423
424        // 4. Sign it using the hardware identity
425        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        // 5. Verify the signature was attached correctly
434        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        // Force timestamps and UUIDs to be identical for deterministic test
466        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        // Tamper with the payload of event2
500        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        // Reconstruct exactly what JCS bytes were signed
536        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, // It was 0 when signed
547            evidence_capsule: &event.evidence_capsule,
548            schema_version: &event.schema_version,
549        };
550        let expected_jcs_bytes = serde_jcs::to_vec(&params).unwrap();
551
552        // Regenerate the signing key strictly to get its verifying (public) key
553        // Note: For a real TPM, you wouldn't be able to extract the private seed,
554        // but since we are in fallback mock mode for testing, we can reconstruct the verifying key from the dummy seed.
555        let signing_key = ed25519_dalek::SigningKey::from_bytes(&dummy_seed);
556        let verifying_key = signing_key.verifying_key();
557
558        // Verify the signature against the JCS payload using the public key
559        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}