reasonkit-core 0.1.8

The Reasoning Engine — Auditable Reasoning for Production AI | Rust-Native | Turn Prompts into Protocols
//! Cryptographic Audit Trail Module
//!
//! This module provides GDPR-compliant audit trail capabilities with:
//! - Ed25519 digital signatures (Quarkslab-audited)
//! - Merkle trees for tamper-evident logging
//! - BLAKE3 content hashing
//!
//! Enable with: `cargo build --features audit-crypto`

use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::time::SystemTime;

// Re-exports
pub use blake3;
pub use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
pub use rs_merkle::{Hasher, MerkleProof, MerkleTree};

/// BLAKE3 hasher for Merkle trees
#[derive(Clone)]
pub struct Blake3Hasher;

impl Hasher for Blake3Hasher {
    type Hash = [u8; 32];

    fn hash(data: &[u8]) -> Self::Hash {
        *blake3::hash(data).as_bytes()
    }
}

/// An audit log entry
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditEntry {
    /// Entry ID
    pub id: String,
    /// Timestamp
    pub timestamp: u64,
    /// Action performed
    pub action: String,
    /// Actor (user/agent ID)
    pub actor: String,
    /// Resource affected
    pub resource: String,
    /// Additional metadata
    pub metadata: serde_json::Value,
    /// Content hash (BLAKE3)
    pub content_hash: String,
}

impl AuditEntry {
    /// Create a new audit entry
    pub fn new(
        action: impl Into<String>,
        actor: impl Into<String>,
        resource: impl Into<String>,
        metadata: serde_json::Value,
    ) -> Self {
        let id = uuid::Uuid::new_v4().to_string();
        let timestamp = SystemTime::now()
            .duration_since(SystemTime::UNIX_EPOCH)
            .unwrap()
            .as_secs();

        let mut entry = Self {
            id,
            timestamp,
            action: action.into(),
            actor: actor.into(),
            resource: resource.into(),
            metadata,
            content_hash: String::new(),
        };

        // Compute content hash
        entry.content_hash = entry.compute_hash();
        entry
    }

    /// Compute the BLAKE3 hash of this entry
    pub fn compute_hash(&self) -> String {
        let content = format!(
            "{}:{}:{}:{}:{}",
            self.id, self.timestamp, self.action, self.actor, self.resource
        );
        let hash = blake3::hash(content.as_bytes());
        hex::encode(hash.as_bytes())
    }

    /// Convert to bytes for signing/merkle
    pub fn to_bytes(&self) -> Vec<u8> {
        serde_json::to_vec(self).unwrap_or_default()
    }
}

/// Signed audit entry with Ed25519 signature
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignedAuditEntry {
    /// The audit entry
    pub entry: AuditEntry,
    /// Ed25519 signature (hex encoded)
    pub signature: String,
    /// Public key of signer (hex encoded)
    pub signer_pubkey: String,
}

impl SignedAuditEntry {
    /// Create a signed audit entry
    pub fn sign(entry: AuditEntry, signing_key: &SigningKey) -> Self {
        let signature = signing_key.sign(&entry.to_bytes());
        let verifying_key = signing_key.verifying_key();

        Self {
            entry,
            signature: hex::encode(signature.to_bytes()),
            signer_pubkey: hex::encode(verifying_key.to_bytes()),
        }
    }

    /// Verify the signature
    pub fn verify(&self) -> Result<bool> {
        let pubkey_bytes: [u8; 32] = hex::decode(&self.signer_pubkey)?
            .try_into()
            .map_err(|_| anyhow::anyhow!("Invalid pubkey length"))?;
        let verifying_key = VerifyingKey::from_bytes(&pubkey_bytes)?;

        let sig_bytes: [u8; 64] = hex::decode(&self.signature)?
            .try_into()
            .map_err(|_| anyhow::anyhow!("Invalid signature length"))?;
        let signature = Signature::from_bytes(&sig_bytes);

        Ok(verifying_key
            .verify(&self.entry.to_bytes(), &signature)
            .is_ok())
    }
}

/// Audit log with Merkle tree integrity
pub struct AuditLog {
    entries: Vec<SignedAuditEntry>,
    signing_key: SigningKey,
}

impl AuditLog {
    /// Create a new audit log
    pub fn new() -> Self {
        let signing_key = SigningKey::generate(&mut rand::thread_rng());
        Self {
            entries: Vec::new(),
            signing_key,
        }
    }

    /// Create from existing signing key
    pub fn with_key(signing_key: SigningKey) -> Self {
        Self {
            entries: Vec::new(),
            signing_key,
        }
    }

    /// Get the public key for this log
    pub fn public_key(&self) -> String {
        hex::encode(self.signing_key.verifying_key().to_bytes())
    }

    /// Append an entry to the log
    pub fn append(&mut self, entry: AuditEntry) -> &SignedAuditEntry {
        let signed = SignedAuditEntry::sign(entry, &self.signing_key);
        self.entries.push(signed);
        self.entries.last().unwrap()
    }

    /// Get the Merkle root of all entries
    pub fn merkle_root(&self) -> Option<String> {
        if self.entries.is_empty() {
            return None;
        }

        let leaves: Vec<[u8; 32]> = self
            .entries
            .iter()
            .map(|e| Blake3Hasher::hash(&e.entry.to_bytes()))
            .collect();

        let tree = MerkleTree::<Blake3Hasher>::from_leaves(&leaves);
        tree.root().map(hex::encode)
    }

    /// Generate a proof for a specific entry
    pub fn proof_for(&self, index: usize) -> Option<Vec<String>> {
        if index >= self.entries.len() {
            return None;
        }

        let leaves: Vec<[u8; 32]> = self
            .entries
            .iter()
            .map(|e| Blake3Hasher::hash(&e.entry.to_bytes()))
            .collect();

        let tree = MerkleTree::<Blake3Hasher>::from_leaves(&leaves);
        let proof = tree.proof(&[index]);
        let proof_hashes: Vec<String> = proof.proof_hashes().iter().map(hex::encode).collect();

        Some(proof_hashes)
    }

    /// Verify all entries in the log
    pub fn verify_all(&self) -> Result<bool> {
        for entry in &self.entries {
            if !entry.verify()? {
                return Ok(false);
            }
        }
        Ok(true)
    }

    /// Get all entries
    pub fn entries(&self) -> &[SignedAuditEntry] {
        &self.entries
    }

    /// Get entry count
    pub fn len(&self) -> usize {
        self.entries.len()
    }

    /// Check if log is empty
    pub fn is_empty(&self) -> bool {
        self.entries.is_empty()
    }
}

impl Default for AuditLog {
    fn default() -> Self {
        Self::new()
    }
}

/// Quick hash function using BLAKE3
pub fn quick_hash(data: &[u8]) -> String {
    hex::encode(blake3::hash(data).as_bytes())
}

/// Quick hash of a string
pub fn hash_string(s: &str) -> String {
    quick_hash(s.as_bytes())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_audit_entry() {
        let entry = AuditEntry::new(
            "CREATE",
            "user-123",
            "document-456",
            serde_json::json!({"details": "test"}),
        );

        assert!(!entry.id.is_empty());
        assert!(!entry.content_hash.is_empty());
    }

    #[test]
    fn test_signed_entry() {
        let signing_key = SigningKey::generate(&mut rand::thread_rng());
        let entry = AuditEntry::new("TEST", "actor", "resource", serde_json::json!({}));
        let signed = SignedAuditEntry::sign(entry, &signing_key);

        assert!(signed.verify().unwrap());
    }

    #[test]
    fn test_audit_log() {
        let mut log = AuditLog::new();

        log.append(AuditEntry::new(
            "CREATE",
            "user1",
            "doc1",
            serde_json::json!({}),
        ));
        log.append(AuditEntry::new(
            "UPDATE",
            "user1",
            "doc1",
            serde_json::json!({}),
        ));
        log.append(AuditEntry::new(
            "DELETE",
            "user2",
            "doc1",
            serde_json::json!({}),
        ));

        assert_eq!(log.len(), 3);
        assert!(log.merkle_root().is_some());
        assert!(log.verify_all().unwrap());
    }

    #[test]
    fn test_merkle_proof() {
        let mut log = AuditLog::new();

        for i in 0..5 {
            log.append(AuditEntry::new(
                format!("ACTION_{}", i),
                "actor",
                "resource",
                serde_json::json!({}),
            ));
        }

        let proof = log.proof_for(2);
        assert!(proof.is_some());
        assert!(!proof.unwrap().is_empty());
    }
}