1use chrono::{DateTime, Utc};
2use serde::{Serialize, Deserialize};
3
4use crate::crypto::{ContentHash, KeyPair};
5use crate::error::{CortexError, CortexResult};
6
7#[derive(Clone, Debug, Serialize, Deserialize)]
9pub struct TibetToken {
10 pub token_id: String,
11 pub parent_id: Option<String>,
12 pub timestamp: DateTime<Utc>,
13
14 pub erin: ContentHash,
16
17 pub eraan: Vec<String>,
19
20 pub eromheen: Eromheen,
22
23 pub erachter: String,
25
26 pub signature: Option<Vec<u8>>,
28}
29
30#[derive(Clone, Debug, Serialize, Deserialize)]
32pub struct Eromheen {
33 pub actor: String,
34 pub jis_level: u8,
35 pub chunks_accessed: usize,
36 pub chunks_denied: usize,
37 pub airlock_session_ms: Option<f64>,
38}
39
40#[derive(Clone, Debug, Serialize, Deserialize)]
42pub struct Provenance {
43 pub chain: Vec<TibetToken>,
44}
45
46impl TibetToken {
47 pub fn new(
48 erin: ContentHash,
49 erachter: impl Into<String>,
50 actor: impl Into<String>,
51 jis_level: u8,
52 ) -> Self {
53 let id = format!("tibet_{}", Utc::now().timestamp_nanos_opt().unwrap_or(0));
54 Self {
55 token_id: id,
56 parent_id: None,
57 timestamp: Utc::now(),
58 erin,
59 eraan: Vec::new(),
60 eromheen: Eromheen {
61 actor: actor.into(),
62 jis_level,
63 chunks_accessed: 0,
64 chunks_denied: 0,
65 airlock_session_ms: None,
66 },
67 erachter: erachter.into(),
68 signature: None,
69 }
70 }
71
72 pub fn with_parent(mut self, parent_id: impl Into<String>) -> Self {
73 self.parent_id = Some(parent_id.into());
74 self
75 }
76
77 pub fn with_access_stats(mut self, accessed: usize, denied: usize) -> Self {
78 self.eromheen.chunks_accessed = accessed;
79 self.eromheen.chunks_denied = denied;
80 self
81 }
82
83 pub fn with_airlock_time(mut self, ms: f64) -> Self {
84 self.eromheen.airlock_session_ms = Some(ms);
85 self
86 }
87
88 pub fn signable_bytes(&self) -> Vec<u8> {
90 let mut token = self.clone();
91 token.signature = None;
92 serde_json::to_vec(&token).unwrap_or_default()
93 }
94
95 pub fn sign(mut self, keypair: &KeyPair) -> Self {
98 let bytes = self.signable_bytes();
99 self.signature = Some(keypair.sign(&bytes));
100 self
101 }
102
103 pub fn verify_signature(&self, keypair: &KeyPair) -> CortexResult<()> {
106 match &self.signature {
107 Some(sig) => keypair.verify(&self.signable_bytes(), sig),
108 None => Err(CortexError::SignatureInvalid),
109 }
110 }
111
112 pub fn is_signed(&self) -> bool {
114 self.signature.is_some()
115 }
116}
117
118impl Provenance {
119 pub fn new() -> Self {
120 Self { chain: Vec::new() }
121 }
122
123 pub fn append(&mut self, token: TibetToken) {
124 self.chain.push(token);
125 }
126
127 pub fn latest(&self) -> Option<&TibetToken> {
128 self.chain.last()
129 }
130
131 pub fn len(&self) -> usize {
132 self.chain.len()
133 }
134
135 pub fn is_empty(&self) -> bool {
136 self.chain.is_empty()
137 }
138
139 pub fn verify_chain(&self) -> bool {
141 for window in self.chain.windows(2) {
142 let parent = &window[0];
143 let child = &window[1];
144 match &child.parent_id {
145 Some(pid) if pid == &parent.token_id => continue,
146 _ => return false,
147 }
148 }
149 true
150 }
151
152 pub fn verify_signatures(&self, keypair: &KeyPair) -> bool {
155 if !self.verify_chain() {
156 return false;
157 }
158 self.chain.iter().all(|t| t.verify_signature(keypair).is_ok())
159 }
160}
161
162impl Default for Provenance {
163 fn default() -> Self {
164 Self::new()
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 #[test]
173 fn test_token_creation() {
174 let hash = ContentHash("sha256:abc123".into());
175 let token = TibetToken::new(hash, "test query", "user@test.com", 2);
176 assert!(token.token_id.starts_with("tibet_"));
177 assert_eq!(token.eromheen.jis_level, 2);
178 assert_eq!(token.erachter, "test query");
179 }
180
181 #[test]
182 fn test_provenance_chain() {
183 let h1 = ContentHash("sha256:aaa".into());
184 let h2 = ContentHash("sha256:bbb".into());
185
186 let t1 = TibetToken::new(h1, "first", "actor", 0);
187 let t1_id = t1.token_id.clone();
188 let t2 = TibetToken::new(h2, "second", "actor", 0).with_parent(t1_id);
189
190 let mut prov = Provenance::new();
191 prov.append(t1);
192 prov.append(t2);
193
194 assert_eq!(prov.len(), 2);
195 assert!(prov.verify_chain());
196 }
197
198 #[test]
199 fn test_broken_chain() {
200 let h1 = ContentHash("sha256:aaa".into());
201 let h2 = ContentHash("sha256:bbb".into());
202
203 let t1 = TibetToken::new(h1, "first", "actor", 0);
204 let t2 = TibetToken::new(h2, "second", "actor", 0).with_parent("wrong_parent");
205
206 let mut prov = Provenance::new();
207 prov.append(t1);
208 prov.append(t2);
209
210 assert!(!prov.verify_chain());
211 }
212
213 #[test]
214 fn test_token_sign_verify() {
215 let kp = KeyPair::generate();
216 let hash = ContentHash::compute(b"test data");
217 let token = TibetToken::new(hash, "signed action", "actor@test", 2).sign(&kp);
218
219 assert!(token.is_signed());
220 assert!(token.verify_signature(&kp).is_ok());
221 }
222
223 #[test]
224 fn test_unsigned_token_fails_verify() {
225 let kp = KeyPair::generate();
226 let hash = ContentHash::compute(b"test");
227 let token = TibetToken::new(hash, "unsigned", "actor", 0);
228
229 assert!(!token.is_signed());
230 assert!(token.verify_signature(&kp).is_err());
231 }
232
233 #[test]
234 fn test_wrong_key_fails_verify() {
235 let kp1 = KeyPair::generate();
236 let kp2 = KeyPair::generate();
237 let hash = ContentHash::compute(b"data");
238 let token = TibetToken::new(hash, "action", "actor", 0).sign(&kp1);
239
240 assert!(token.verify_signature(&kp2).is_err());
241 }
242
243 #[test]
244 fn test_signed_chain_verify() {
245 let kp = KeyPair::generate();
246 let h1 = ContentHash::compute(b"first");
247 let h2 = ContentHash::compute(b"second");
248 let h3 = ContentHash::compute(b"third");
249
250 let t1 = TibetToken::new(h1, "first", "actor", 0).sign(&kp);
251 let t1_id = t1.token_id.clone();
252 let t2 = TibetToken::new(h2, "second", "actor", 0)
253 .with_parent(&t1_id)
254 .sign(&kp);
255 let t2_id = t2.token_id.clone();
256 let t3 = TibetToken::new(h3, "third", "actor", 0)
257 .with_parent(&t2_id)
258 .sign(&kp);
259
260 let mut prov = Provenance::new();
261 prov.append(t1);
262 prov.append(t2);
263 prov.append(t3);
264
265 assert!(prov.verify_chain());
266 assert!(prov.verify_signatures(&kp));
267 }
268
269 #[test]
270 fn test_mixed_signed_unsigned_chain_fails() {
271 let kp = KeyPair::generate();
272 let h1 = ContentHash::compute(b"first");
273 let h2 = ContentHash::compute(b"second");
274
275 let t1 = TibetToken::new(h1, "first", "actor", 0).sign(&kp);
276 let t1_id = t1.token_id.clone();
277 let t2 = TibetToken::new(h2, "second", "actor", 0).with_parent(&t1_id);
279
280 let mut prov = Provenance::new();
281 prov.append(t1);
282 prov.append(t2);
283
284 assert!(prov.verify_chain()); assert!(!prov.verify_signatures(&kp)); }
287}