_hope_core/
proof.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3
4/// Type of action being performed
5#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
6pub enum ActionType {
7    Delete,
8    Write,
9    Read,
10    Execute,
11    Network,
12    Custom(String),
13}
14
15/// Verification status of a proof
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17pub enum VerificationStatus {
18    OK,
19    Tampered,
20    Failed(String),
21}
22
23/// Action that can be performed by the AI
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct Action {
26    pub action_type: ActionType,
27    pub target: String,
28    pub payload: Option<Vec<u8>>,
29    pub metadata: Option<String>,
30}
31
32impl Action {
33    pub fn delete(target: impl Into<String>) -> Self {
34        Action {
35            action_type: ActionType::Delete,
36            target: target.into(),
37            payload: None,
38            metadata: None,
39        }
40    }
41
42    pub fn write_file(path: impl Into<String>, content: Vec<u8>) -> Self {
43        Action {
44            action_type: ActionType::Write,
45            target: path.into(),
46            payload: Some(content),
47            metadata: None,
48        }
49    }
50
51    pub fn read(target: impl Into<String>) -> Self {
52        Action {
53            action_type: ActionType::Read,
54            target: target.into(),
55            payload: None,
56            metadata: None,
57        }
58    }
59
60    pub fn execute(command: impl Into<String>) -> Self {
61        Action {
62            action_type: ActionType::Execute,
63            target: command.into(),
64            payload: None,
65            metadata: None,
66        }
67    }
68
69    /// Get the canonical hash of this action
70    ///
71    /// # Panics
72    /// Only panics in extreme OOM conditions (memory exhaustion).
73    /// In practice, Action serialization is infallible for normal structs.
74    ///
75    /// # v1.6.0 Security Enhancements
76    /// - **H-2**: Changed from `.unwrap()` to `.expect()` with descriptive message
77    /// - **M-1**: Migrated from JSON to bincode for guaranteed deterministic serialization
78    ///   - JSON field order was implementation-defined (fragile across versions)
79    ///   - Bincode guarantees byte-for-byte determinism (same Action = same hash)
80    ///   - Critical for cross-platform and cross-version action binding security
81    pub fn hash(&self) -> [u8; 32] {
82        let serialized = bincode::serialize(self)
83            .expect("Action serialization failed - this indicates extreme memory exhaustion (OOM)");
84        crate::crypto::hash_bytes(&serialized)
85    }
86}
87
88/// Cryptographic proof of action integrity
89///
90/// This struct provides tamper-evident proof that an action was approved
91/// by the Hope Genome system. It includes:
92/// - Anti-replay protection (nonce + TTL)
93/// - Action binding (prevents oracle attacks)
94/// - Cryptographic signature (prevents forgery)
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct IntegrityProof {
97    /// Anti-replay nonce (256-bit random)
98    pub nonce: [u8; 32],
99
100    /// Unix timestamp (seconds) when proof was created
101    pub timestamp: u64,
102
103    /// Time-to-live in seconds (proof expires after this)
104    pub ttl: u64,
105
106    /// Hash of the action being approved (prevents oracle attacks)
107    pub action_hash: [u8; 32],
108
109    /// Type of action
110    pub action_type: ActionType,
111
112    /// Current genome capsule hash (ties proof to specific genome state)
113    pub capsule_hash: String,
114
115    /// Verification status
116    pub status: VerificationStatus,
117
118    /// RSA signature (signs all above fields)
119    pub signature: Vec<u8>,
120}
121
122impl IntegrityProof {
123    /// Create a new proof (before signing)
124    pub fn new(action: &Action, capsule_hash: String, ttl: u64) -> Self {
125        let nonce = crate::crypto::generate_nonce();
126        let timestamp = chrono::Utc::now().timestamp() as u64;
127
128        IntegrityProof {
129            nonce,
130            timestamp,
131            ttl,
132            action_hash: action.hash(),
133            action_type: action.action_type.clone(),
134            capsule_hash,
135            status: VerificationStatus::OK,
136            signature: Vec::new(), // Will be filled by signing
137        }
138    }
139
140    /// Get the data that should be signed
141    pub fn signing_data(&self) -> Vec<u8> {
142        // Serialize everything except signature
143        let mut data = Vec::new();
144        data.extend_from_slice(&self.nonce);
145        data.extend_from_slice(&self.timestamp.to_le_bytes());
146        data.extend_from_slice(&self.ttl.to_le_bytes());
147        data.extend_from_slice(&self.action_hash);
148        data.extend_from_slice(self.capsule_hash.as_bytes());
149        data
150    }
151
152    /// Check if proof has expired
153    ///
154    /// # v1.6.0 Note
155    /// H-1 fix: Uses saturating_sub() to prevent integer underflow.
156    pub fn is_expired(&self) -> bool {
157        let now = chrono::Utc::now().timestamp() as u64;
158        let elapsed = now.saturating_sub(self.timestamp);
159        elapsed > self.ttl
160    }
161
162    /// Get human-readable timestamp
163    pub fn timestamp_string(&self) -> String {
164        let dt = DateTime::from_timestamp(self.timestamp as i64, 0).unwrap_or_else(Utc::now);
165        dt.to_rfc3339()
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn test_action_hash_deterministic() {
175        let action = Action::delete("test.txt");
176        let hash1 = action.hash();
177        let hash2 = action.hash();
178        assert_eq!(hash1, hash2);
179    }
180
181    #[test]
182    fn test_different_actions_different_hashes() {
183        let action1 = Action::delete("test1.txt");
184        let action2 = Action::delete("test2.txt");
185        assert_ne!(action1.hash(), action2.hash());
186    }
187
188    #[test]
189    fn test_proof_nonce_uniqueness() {
190        let action = Action::delete("test.txt");
191        let proof1 = IntegrityProof::new(&action, "hash1".into(), 60);
192        let proof2 = IntegrityProof::new(&action, "hash1".into(), 60);
193        assert_ne!(proof1.nonce, proof2.nonce);
194    }
195
196    #[test]
197    fn test_proof_expiration() {
198        let action = Action::delete("test.txt");
199        let mut proof = IntegrityProof::new(&action, "hash1".into(), 0);
200
201        // Set timestamp to 10 seconds ago with 5 second TTL
202        proof.timestamp = chrono::Utc::now().timestamp() as u64 - 10;
203        proof.ttl = 5;
204
205        assert!(proof.is_expired());
206    }
207
208    #[test]
209    fn test_proof_not_expired() {
210        let action = Action::delete("test.txt");
211        let proof = IntegrityProof::new(&action, "hash1".into(), 3600);
212        assert!(!proof.is_expired());
213    }
214}