1use serde::{Deserialize, Serialize};
13use thiserror::Error;
14
15use crate::authorize::{Decision, DenyReason};
16use crate::capability::{Ops, Scope};
17use crate::identity::{verify_sec1, Did, Identity};
18
19pub type AuditResult<T> = Result<T, AuditError>;
20
21#[derive(Debug, Error)]
22pub enum AuditError {
23 #[error("audit canonicalization: {0}")]
24 Canonical(String),
25 #[error("audit chain broken at seq {0}")]
26 BrokenChain(u64),
27 #[error("audit entry hash mismatch at seq {0}")]
28 HashMismatch(u64),
29 #[error("audit node signature invalid at seq {0}")]
30 BadSignature(u64),
31 #[error("audit sequence mismatch at index {0}")]
32 SeqMismatch(u64),
33}
34
35#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
37pub enum AuditDecision {
38 Allowed,
39 Denied(DenyReason),
40}
41
42impl AuditDecision {
43 pub fn of(decision: &Decision) -> Self {
45 match decision {
46 Decision::Allow => AuditDecision::Allowed,
47 Decision::Deny(reason) => AuditDecision::Denied(reason.clone()),
48 }
49 }
50}
51
52#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
54pub struct AuditEntry {
55 pub seq: u64,
56 pub prev_hash: [u8; 32],
58 pub at_unix: u64,
59 pub bearer: Did,
60 pub scope: Scope,
61 pub op: Ops,
62 pub capability_id: Option<[u8; 16]>,
63 pub decision: AuditDecision,
64 pub entry_hash: [u8; 32],
66 pub node_signature: Vec<u8>,
68}
69
70fn content_bytes(
71 seq: u64,
72 prev_hash: &[u8; 32],
73 at_unix: u64,
74 bearer: &Did,
75 scope: &Scope,
76 op: Ops,
77 capability_id: &Option<[u8; 16]>,
78 decision: &AuditDecision,
79) -> AuditResult<Vec<u8>> {
80 postcard::to_allocvec(&(seq, prev_hash, at_unix, bearer, scope, op, capability_id, decision))
81 .map_err(|e| AuditError::Canonical(e.to_string()))
82}
83
84fn hash(bytes: &[u8]) -> [u8; 32] {
85 *blake3::hash(bytes).as_bytes()
86}
87
88#[derive(Clone, Debug, Default)]
90pub struct AuditLog {
91 entries: Vec<AuditEntry>,
92}
93
94impl AuditLog {
95 pub fn new() -> Self {
96 Self::default()
97 }
98
99 pub fn from_entries(entries: Vec<AuditEntry>) -> Self {
101 Self { entries }
102 }
103
104 pub fn entries(&self) -> &[AuditEntry] {
105 &self.entries
106 }
107
108 pub fn len(&self) -> usize {
109 self.entries.len()
110 }
111
112 pub fn is_empty(&self) -> bool {
113 self.entries.is_empty()
114 }
115
116 #[allow(clippy::too_many_arguments)]
118 pub fn record(
119 &mut self,
120 node: &Identity,
121 at_unix: u64,
122 bearer: &Did,
123 scope: &Scope,
124 op: Ops,
125 capability_id: Option<[u8; 16]>,
126 decision: AuditDecision,
127 ) -> AuditResult<()> {
128 let seq = self.entries.len() as u64;
129 let prev_hash = self
130 .entries
131 .last()
132 .map(|e| e.entry_hash)
133 .unwrap_or([0u8; 32]);
134
135 let canonical = content_bytes(
136 seq,
137 &prev_hash,
138 at_unix,
139 bearer,
140 scope,
141 op,
142 &capability_id,
143 &decision,
144 )?;
145 let entry_hash = hash(&canonical);
146 let node_signature = node.sign(&canonical);
147
148 self.entries.push(AuditEntry {
149 seq,
150 prev_hash,
151 at_unix,
152 bearer: bearer.clone(),
153 scope: scope.clone(),
154 op,
155 capability_id,
156 decision,
157 entry_hash,
158 node_signature,
159 });
160 Ok(())
161 }
162
163 pub fn verify(&self, node_public_key: &[u8]) -> AuditResult<()> {
166 let mut expected_prev = [0u8; 32];
167 for (index, entry) in self.entries.iter().enumerate() {
168 if entry.seq != index as u64 {
169 return Err(AuditError::SeqMismatch(index as u64));
170 }
171 if entry.prev_hash != expected_prev {
172 return Err(AuditError::BrokenChain(entry.seq));
173 }
174 let canonical = content_bytes(
175 entry.seq,
176 &entry.prev_hash,
177 entry.at_unix,
178 &entry.bearer,
179 &entry.scope,
180 entry.op,
181 &entry.capability_id,
182 &entry.decision,
183 )?;
184 if hash(&canonical) != entry.entry_hash {
185 return Err(AuditError::HashMismatch(entry.seq));
186 }
187 if !verify_sec1(node_public_key, &canonical, &entry.node_signature) {
188 return Err(AuditError::BadSignature(entry.seq));
189 }
190 expected_prev = entry.entry_hash;
191 }
192 Ok(())
193 }
194}