libgrite_core/
integrity.rs1use crate::hash::compute_event_id;
6use crate::signing::verify_signature;
7use crate::store::GriteStore;
8use crate::types::event::Event;
9use crate::types::ids::{id_to_hex, EventId};
10use crate::GriteError;
11
12#[derive(Debug, Default)]
14pub struct IntegrityReport {
15 pub events_checked: usize,
17 pub events_valid: usize,
19 pub corrupt_events: Vec<CorruptEvent>,
21 pub signatures_checked: usize,
23 pub signatures_valid: usize,
25 pub signature_errors: Vec<SignatureError>,
27}
28
29#[derive(Debug)]
31pub struct CorruptEvent {
32 pub event_id: EventId,
33 pub issue_id: String,
34 pub kind: CorruptionKind,
35}
36
37#[derive(Debug)]
39pub enum CorruptionKind {
40 HashMismatch {
42 expected: EventId,
43 computed: EventId,
44 },
45 MissingParent { parent_id: EventId },
47}
48
49#[derive(Debug)]
51pub struct SignatureError {
52 pub event_id: EventId,
53 pub actor_id: String,
54 pub error: String,
55}
56
57impl IntegrityReport {
58 pub fn is_healthy(&self) -> bool {
60 self.corrupt_events.is_empty() && self.signature_errors.is_empty()
61 }
62
63 pub fn corruption_count(&self) -> usize {
65 self.corrupt_events.len()
66 }
67
68 pub fn signature_error_count(&self) -> usize {
70 self.signature_errors.len()
71 }
72}
73
74pub fn verify_event_hash(event: &Event) -> Result<(), CorruptionKind> {
76 let computed = compute_event_id(
77 &event.issue_id,
78 &event.actor,
79 event.ts_unix_ms,
80 event.parent.as_ref(),
81 &event.kind,
82 );
83
84 if computed != event.event_id {
85 return Err(CorruptionKind::HashMismatch {
86 expected: event.event_id,
87 computed,
88 });
89 }
90
91 Ok(())
92}
93
94pub fn check_store_integrity(
100 store: &GriteStore,
101 verify_parents: bool,
102) -> Result<IntegrityReport, GriteError> {
103 let mut report = IntegrityReport::default();
104
105 let issues = store.list_issues(&Default::default())?;
107
108 for issue_summary in &issues {
109 let events = store.get_issue_events(&issue_summary.issue_id)?;
110 let event_ids: std::collections::HashSet<EventId> =
111 events.iter().map(|e| e.event_id).collect();
112
113 for event in &events {
114 report.events_checked += 1;
115
116 match verify_event_hash(event) {
118 Ok(()) => {
119 report.events_valid += 1;
120 }
121 Err(kind) => {
122 report.corrupt_events.push(CorruptEvent {
123 event_id: event.event_id,
124 issue_id: id_to_hex(&event.issue_id),
125 kind,
126 });
127 continue;
128 }
129 }
130
131 if verify_parents {
133 if let Some(parent_id) = &event.parent {
134 if !event_ids.contains(parent_id) {
135 report.corrupt_events.push(CorruptEvent {
136 event_id: event.event_id,
137 issue_id: id_to_hex(&event.issue_id),
138 kind: CorruptionKind::MissingParent {
139 parent_id: *parent_id,
140 },
141 });
142 }
143 }
144 }
145 }
146 }
147
148 Ok(report)
149}
150
151pub fn verify_store_signatures<F>(
155 store: &GriteStore,
156 get_public_key: F,
157) -> Result<IntegrityReport, GriteError>
158where
159 F: Fn(&str) -> Option<String>,
160{
161 let mut report = IntegrityReport::default();
162
163 let issues = store.list_issues(&Default::default())?;
164
165 for issue_summary in &issues {
166 let events = store.get_issue_events(&issue_summary.issue_id)?;
167
168 for event in &events {
169 report.events_checked += 1;
170
171 if event.sig.is_none() {
173 report.signature_errors.push(SignatureError {
174 event_id: event.event_id,
175 actor_id: id_to_hex(&event.actor),
176 error: "signature missing".to_string(),
177 });
178 continue;
179 }
180
181 report.signatures_checked += 1;
182
183 let actor_hex = id_to_hex(&event.actor);
185 let public_key = match get_public_key(&actor_hex) {
186 Some(pk) => pk,
187 None => {
188 report.signature_errors.push(SignatureError {
189 event_id: event.event_id,
190 actor_id: actor_hex,
191 error: "public key not found".to_string(),
192 });
193 continue;
194 }
195 };
196
197 match verify_signature(event, &public_key) {
199 Ok(()) => {
200 report.signatures_valid += 1;
201 report.events_valid += 1;
202 }
203 Err(e) => {
204 report.signature_errors.push(SignatureError {
205 event_id: event.event_id,
206 actor_id: actor_hex,
207 error: e.to_string(),
208 });
209 }
210 }
211 }
212 }
213
214 Ok(report)
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220 use crate::types::event::EventKind;
221
222 #[test]
223 fn test_verify_event_hash_valid() {
224 let issue_id = [1u8; 16];
225 let actor = [2u8; 16];
226 let ts = 1700000000000u64;
227 let kind = EventKind::IssueCreated {
228 title: "Test".to_string(),
229 body: "Body".to_string(),
230 labels: vec![],
231 };
232
233 let event_id = compute_event_id(&issue_id, &actor, ts, None, &kind);
234 let event = Event::new(event_id, issue_id, actor, ts, None, kind);
235
236 assert!(verify_event_hash(&event).is_ok());
237 }
238
239 #[test]
240 fn test_verify_event_hash_invalid() {
241 let issue_id = [1u8; 16];
242 let actor = [2u8; 16];
243 let ts = 1700000000000u64;
244 let kind = EventKind::IssueCreated {
245 title: "Test".to_string(),
246 body: "Body".to_string(),
247 labels: vec![],
248 };
249
250 let event = Event::new([0u8; 32], issue_id, actor, ts, None, kind);
252
253 let result = verify_event_hash(&event);
254 assert!(matches!(result, Err(CorruptionKind::HashMismatch { .. })));
255 }
256
257 #[test]
258 fn test_integrity_report_is_healthy() {
259 let report = IntegrityReport::default();
260 assert!(report.is_healthy());
261
262 let mut report_with_error = IntegrityReport::default();
263 report_with_error.corrupt_events.push(CorruptEvent {
264 event_id: [0u8; 32],
265 issue_id: "test".to_string(),
266 kind: CorruptionKind::HashMismatch {
267 expected: [0u8; 32],
268 computed: [1u8; 32],
269 },
270 });
271 assert!(!report_with_error.is_healthy());
272 }
273}