1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub enum AuditAction {
11 CredentialIssued,
12 CredentialPresented,
13 CredentialVerified,
14 CredentialRevoked,
15 ConsentGiven,
16 ConsentRevoked,
17 DidCreated,
18 DidResolved,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct AuditEntry {
24 pub id: String,
26 pub sequence: u64,
28 pub timestamp: String,
30 pub action: AuditAction,
32 pub actor: String,
34 pub details: serde_json::Value,
36 pub previous_hash: String,
38}
39
40impl AuditEntry {
41 pub fn hash(&self) -> String {
43 use std::fmt::Write;
44 let data = serde_json::to_string(self).unwrap_or_default();
46 let digest = simple_sha256(data.as_bytes());
47 let mut hex = String::with_capacity(64);
48 for b in &digest {
49 write!(hex, "{b:02x}").unwrap();
50 }
51 hex
52 }
53}
54
55fn simple_sha256(data: &[u8]) -> [u8; 32] {
57 let mut state: [u32; 8] = [
66 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab,
67 0x5be0cd19,
68 ];
69 for (i, &byte) in data.iter().enumerate() {
70 let idx = i % 8;
71 state[idx] = state[idx]
72 .wrapping_mul(31)
73 .wrapping_add(byte as u32)
74 .wrapping_add(state[(idx + 1) % 8]);
75 }
76 let mut result = [0u8; 32];
77 for (i, val) in state.iter().enumerate() {
78 result[i * 4..i * 4 + 4].copy_from_slice(&val.to_be_bytes());
79 }
80 result
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
85pub enum RedactableField {
86 Actor,
88 Details,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct RedactionPolicy {
95 pub redact: Vec<RedactableField>,
97 pub replacement: String,
99}
100
101impl Default for RedactionPolicy {
102 fn default() -> Self {
103 Self {
104 redact: vec![],
105 replacement: "[REDACTED]".to_string(),
106 }
107 }
108}
109
110pub struct AuditLog {
112 entries: Vec<AuditEntry>,
113}
114
115impl AuditLog {
116 pub fn new() -> Self {
118 Self {
119 entries: Vec::new(),
120 }
121 }
122
123 pub fn append(
125 &mut self,
126 id: impl Into<String>,
127 timestamp: impl Into<String>,
128 action: AuditAction,
129 actor: impl Into<String>,
130 details: serde_json::Value,
131 ) -> &AuditEntry {
132 let previous_hash = self.entries.last().map(|e| e.hash()).unwrap_or_default();
133 let sequence = self.entries.len() as u64;
134
135 self.entries.push(AuditEntry {
136 id: id.into(),
137 sequence,
138 timestamp: timestamp.into(),
139 action,
140 actor: actor.into(),
141 details,
142 previous_hash,
143 });
144
145 self.entries.last().unwrap()
146 }
147
148 pub fn entries(&self) -> &[AuditEntry] {
150 &self.entries
151 }
152
153 pub fn len(&self) -> usize {
155 self.entries.len()
156 }
157
158 pub fn is_empty(&self) -> bool {
160 self.entries.is_empty()
161 }
162
163 pub fn verify_chain(&self) -> bool {
165 for i in 1..self.entries.len() {
166 let expected_hash = self.entries[i - 1].hash();
167 if self.entries[i].previous_hash != expected_hash {
168 return false;
169 }
170 }
171 if let Some(first) = self.entries.first() {
173 if !first.previous_hash.is_empty() {
174 return false;
175 }
176 }
177 true
178 }
179
180 pub fn by_actor(&self, actor: &str) -> Vec<&AuditEntry> {
182 self.entries.iter().filter(|e| e.actor == actor).collect()
183 }
184
185 pub fn by_action(&self, action: AuditAction) -> Vec<&AuditEntry> {
187 self.entries.iter().filter(|e| e.action == action).collect()
188 }
189
190 pub fn by_time_range(&self, from: &str, to: &str) -> Vec<&AuditEntry> {
192 self.entries
193 .iter()
194 .filter(|e| e.timestamp.as_str() >= from && e.timestamp.as_str() <= to)
195 .collect()
196 }
197
198 pub fn export(&self, policy: &RedactionPolicy) -> String {
200 let mut lines = Vec::new();
201 for entry in &self.entries {
202 let mut obj = serde_json::to_value(entry).unwrap();
203 if let Some(map) = obj.as_object_mut() {
204 for field in &policy.redact {
205 match field {
206 RedactableField::Actor => {
207 map.insert(
208 "actor".to_string(),
209 serde_json::Value::String(policy.replacement.clone()),
210 );
211 }
212 RedactableField::Details => {
213 map.insert(
214 "details".to_string(),
215 serde_json::Value::String(policy.replacement.clone()),
216 );
217 }
218 }
219 }
220 }
221 lines.push(serde_json::to_string(&obj).unwrap());
222 }
223 lines.join("\n")
224 }
225}
226
227impl Default for AuditLog {
228 fn default() -> Self {
229 Self::new()
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236 use serde_json::json;
237
238 fn build_log() -> AuditLog {
239 let mut log = AuditLog::new();
240 log.append(
241 "entry-1",
242 "2026-03-01T10:00:00Z",
243 AuditAction::DidCreated,
244 "did:key:z6MkIssuer",
245 json!({"method": "did:key"}),
246 );
247 log.append(
248 "entry-2",
249 "2026-03-01T11:00:00Z",
250 AuditAction::CredentialIssued,
251 "did:key:z6MkIssuer",
252 json!({"type": "CanadianDigitalID", "subject": "did:key:z6MkHolder"}),
253 );
254 log.append(
255 "entry-3",
256 "2026-03-02T09:00:00Z",
257 AuditAction::ConsentGiven,
258 "did:key:z6MkHolder",
259 json!({"verifier": "did:key:z6MkVerifier", "purpose": "age-check"}),
260 );
261 log.append(
262 "entry-4",
263 "2026-03-02T09:01:00Z",
264 AuditAction::CredentialPresented,
265 "did:key:z6MkHolder",
266 json!({"verifier": "did:key:z6MkVerifier"}),
267 );
268 log
269 }
270
271 #[test]
272 fn append_sets_sequence_and_hash() {
273 let log = build_log();
274 assert_eq!(log.len(), 4);
275 assert_eq!(log.entries()[0].sequence, 0);
276 assert_eq!(log.entries()[1].sequence, 1);
277 assert_eq!(log.entries()[0].previous_hash, "");
278 assert!(!log.entries()[1].previous_hash.is_empty());
279 }
280
281 #[test]
282 fn hash_chain_verifies() {
283 let log = build_log();
284 assert!(log.verify_chain());
285 }
286
287 #[test]
288 fn tampered_chain_fails_verification() {
289 let mut log = build_log();
290 log.entries[1].actor = "tampered".to_string();
292 assert!(!log.verify_chain());
295 }
296
297 #[test]
298 fn query_by_actor() {
299 let log = build_log();
300 let issuer_entries = log.by_actor("did:key:z6MkIssuer");
301 assert_eq!(issuer_entries.len(), 2);
302 let holder_entries = log.by_actor("did:key:z6MkHolder");
303 assert_eq!(holder_entries.len(), 2);
304 }
305
306 #[test]
307 fn query_by_action() {
308 let log = build_log();
309 let issued = log.by_action(AuditAction::CredentialIssued);
310 assert_eq!(issued.len(), 1);
311 assert_eq!(issued[0].id, "entry-2");
312 }
313
314 #[test]
315 fn query_by_time_range() {
316 let log = build_log();
317 let march1 = log.by_time_range("2026-03-01T00:00:00Z", "2026-03-01T23:59:59Z");
318 assert_eq!(march1.len(), 2);
319 let march2 = log.by_time_range("2026-03-02T00:00:00Z", "2026-03-02T23:59:59Z");
320 assert_eq!(march2.len(), 2);
321 }
322
323 #[test]
324 fn export_without_redaction() {
325 let log = build_log();
326 let exported = log.export(&RedactionPolicy::default());
327 let lines: Vec<&str> = exported.lines().collect();
328 assert_eq!(lines.len(), 4);
329 for line in &lines {
331 let _: serde_json::Value = serde_json::from_str(line).unwrap();
332 }
333 }
334
335 #[test]
336 fn export_with_actor_redaction() {
337 let log = build_log();
338 let policy = RedactionPolicy {
339 redact: vec![RedactableField::Actor],
340 replacement: "[REDACTED]".to_string(),
341 };
342 let exported = log.export(&policy);
343 for line in exported.lines() {
344 let entry: serde_json::Value = serde_json::from_str(line).unwrap();
345 assert_eq!(entry["actor"], "[REDACTED]");
346 }
347 }
348
349 #[test]
350 fn export_with_details_redaction() {
351 let log = build_log();
352 let policy = RedactionPolicy {
353 redact: vec![RedactableField::Details],
354 replacement: "***".to_string(),
355 };
356 let exported = log.export(&policy);
357 for line in exported.lines() {
358 let entry: serde_json::Value = serde_json::from_str(line).unwrap();
359 assert_eq!(entry["details"], "***");
360 }
361 }
362
363 #[test]
364 fn audit_action_serde_roundtrip() {
365 let actions = vec![
366 AuditAction::CredentialIssued,
367 AuditAction::CredentialPresented,
368 AuditAction::CredentialVerified,
369 AuditAction::CredentialRevoked,
370 AuditAction::ConsentGiven,
371 AuditAction::ConsentRevoked,
372 AuditAction::DidCreated,
373 AuditAction::DidResolved,
374 ];
375 for action in &actions {
376 let json = serde_json::to_string(action).unwrap();
377 let back: AuditAction = serde_json::from_str(&json).unwrap();
378 assert_eq!(*action, back);
379 }
380 }
381
382 #[test]
383 fn empty_log_verifies() {
384 let log = AuditLog::new();
385 assert!(log.verify_chain());
386 assert!(log.is_empty());
387 }
388}