1use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
19use serde::{Deserialize, Serialize};
20use sha2::{Digest, Sha256};
21
22const GENESIS_HASH: &str = "0000000000000000000000000000000000000000000000000000000000000000";
24
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27pub struct LedgerEntry {
28 pub seq: u64,
30 pub prev_hash_hex: String,
32 pub entry_hash_hex: String,
35 pub kind: String,
37 pub payload_json: String,
39 pub signer_pubkey_hex: String,
41 pub signature_hex: String,
43 pub time_unix: u64,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(rename_all = "snake_case")]
49pub enum LedgerTamper {
50 BrokenChain { seq: u64 },
52 ContentModified { seq: u64 },
54 BadSignature { seq: u64 },
56 BadSequence { seq: u64 },
58}
59
60#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
62pub struct AccountabilityLedger {
63 entries: Vec<LedgerEntry>,
64}
65
66fn content_hash(
68 seq: u64,
69 prev_hash_hex: &str,
70 kind: &str,
71 payload_json: &str,
72 signer_pubkey_hex: &str,
73 time_unix: u64,
74) -> [u8; 32] {
75 let mut h = Sha256::new();
76 h.update(seq.to_le_bytes());
77 h.update(b"\x1f");
78 h.update(prev_hash_hex.as_bytes());
79 h.update(b"\x1f");
80 h.update(kind.as_bytes());
81 h.update(b"\x1f");
82 h.update(payload_json.as_bytes());
83 h.update(b"\x1f");
84 h.update(signer_pubkey_hex.as_bytes());
85 h.update(b"\x1f");
86 h.update(time_unix.to_le_bytes());
87 h.finalize().into()
88}
89
90impl AccountabilityLedger {
91 pub fn new() -> Self {
92 Self::default()
93 }
94
95 pub fn entries(&self) -> &[LedgerEntry] {
96 &self.entries
97 }
98 pub fn len(&self) -> usize {
99 self.entries.len()
100 }
101 pub fn is_empty(&self) -> bool {
102 self.entries.is_empty()
103 }
104
105 pub fn head_hash(&self) -> String {
107 self.entries
108 .last()
109 .map(|e| e.entry_hash_hex.clone())
110 .unwrap_or_else(|| GENESIS_HASH.to_string())
111 }
112
113 pub fn append(
115 &mut self,
116 kind: impl Into<String>,
117 payload_json: impl Into<String>,
118 signer: &SigningKey,
119 time_unix: u64,
120 ) -> &LedgerEntry {
121 let kind = kind.into();
122 let payload_json = payload_json.into();
123 let seq = self.entries.len() as u64;
124 let prev_hash_hex = self.head_hash();
125 let signer_pubkey_hex = hex::encode(signer.verifying_key().to_bytes());
126
127 let hash = content_hash(
128 seq,
129 &prev_hash_hex,
130 &kind,
131 &payload_json,
132 &signer_pubkey_hex,
133 time_unix,
134 );
135 let signature = signer.sign(&hash);
136
137 self.entries.push(LedgerEntry {
138 seq,
139 prev_hash_hex,
140 entry_hash_hex: hex::encode(hash),
141 kind,
142 payload_json,
143 signer_pubkey_hex,
144 signature_hex: hex::encode(signature.to_bytes()),
145 time_unix,
146 });
147 self.entries.last().expect("just pushed")
148 }
149
150 pub fn verify(&self) -> Result<(), LedgerTamper> {
153 let mut expected_prev = GENESIS_HASH.to_string();
154 for (i, e) in self.entries.iter().enumerate() {
155 if e.seq != i as u64 {
156 return Err(LedgerTamper::BadSequence { seq: e.seq });
157 }
158 if e.prev_hash_hex != expected_prev {
159 return Err(LedgerTamper::BrokenChain { seq: e.seq });
160 }
161 let hash = content_hash(
162 e.seq,
163 &e.prev_hash_hex,
164 &e.kind,
165 &e.payload_json,
166 &e.signer_pubkey_hex,
167 e.time_unix,
168 );
169 if hex::encode(hash) != e.entry_hash_hex {
170 return Err(LedgerTamper::ContentModified { seq: e.seq });
171 }
172 if !verify_signature(&e.signer_pubkey_hex, &hash, &e.signature_hex) {
173 return Err(LedgerTamper::BadSignature { seq: e.seq });
174 }
175 expected_prev = e.entry_hash_hex.clone();
176 }
177 Ok(())
178 }
179
180 pub fn of_kind<'a>(&'a self, kind: &str) -> Vec<&'a LedgerEntry> {
182 self.entries.iter().filter(|e| e.kind == kind).collect()
183 }
184}
185
186fn verify_signature(signer_pubkey_hex: &str, hash: &[u8; 32], signature_hex: &str) -> bool {
187 let Ok(pk_bytes) = hex::decode(signer_pubkey_hex) else {
188 return false;
189 };
190 let Ok(pk_arr): Result<[u8; 32], _> = pk_bytes.as_slice().try_into() else {
191 return false;
192 };
193 let Ok(vk) = VerifyingKey::from_bytes(&pk_arr) else {
194 return false;
195 };
196 let Ok(sig_bytes) = hex::decode(signature_hex) else {
197 return false;
198 };
199 let Ok(sig_arr): Result<[u8; 64], _> = sig_bytes.as_slice().try_into() else {
200 return false;
201 };
202 vk.verify(hash, &Signature::from_bytes(&sig_arr)).is_ok()
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 fn signer(seed: u8) -> SigningKey {
210 SigningKey::from_bytes(&[seed; 32])
211 }
212
213 #[test]
214 fn appends_are_signed_chained_and_verify() {
215 let sk = signer(1);
216 let mut led = AccountabilityLedger::new();
217 led.append("conduct", r#"{"act":"accessed record"}"#, &sk, 1_000);
218 led.append("disclosure", r#"{"to":"did:wf:mp"}"#, &sk, 1_100);
219 led.append("conduct", r#"{"act":"requested placement"}"#, &sk, 1_200);
220
221 assert_eq!(led.len(), 3);
222 assert_eq!(led.verify(), Ok(()), "a well-formed chain verifies");
223 assert!(led
225 .entries()
226 .iter()
227 .all(|e| e.signer_pubkey_hex == hex::encode(sk.verifying_key().to_bytes())));
228 assert_eq!(
230 led.entries()[1].prev_hash_hex,
231 led.entries()[0].entry_hash_hex
232 );
233 assert_eq!(led.of_kind("conduct").len(), 2);
234 }
235
236 #[test]
237 fn modifying_an_entrys_content_is_detected() {
238 let sk = signer(1);
239 let mut led = AccountabilityLedger::new();
240 led.append("conduct", r#"{"act":"accessed record"}"#, &sk, 1_000);
241 led.append("conduct", r#"{"act":"did nothing"}"#, &sk, 1_100);
242 led.entries[1].payload_json = r#"{"act":"acted diligently"}"#.to_string();
244 assert_eq!(led.verify(), Err(LedgerTamper::ContentModified { seq: 1 }));
245 }
246
247 #[test]
248 fn deleting_an_entry_breaks_the_chain() {
249 let sk = signer(1);
250 let mut led = AccountabilityLedger::new();
251 led.append("conduct", "a", &sk, 1_000);
252 led.append("disclosure", "b", &sk, 1_100); led.append("conduct", "c", &sk, 1_200);
254 led.entries.remove(1);
256 assert!(matches!(
258 led.verify(),
259 Err(LedgerTamper::BadSequence { .. }) | Err(LedgerTamper::BrokenChain { .. })
260 ));
261 }
262
263 #[test]
264 fn a_forged_signature_is_detected() {
265 let sk = signer(1);
266 let attacker = signer(2);
267 let mut led = AccountabilityLedger::new();
268 led.append("conduct", r#"{"act":"x"}"#, &sk, 1_000);
269 let e = &mut led.entries[0];
272 e.payload_json = r#"{"act":"forged"}"#.to_string();
273 let hash = content_hash(
274 e.seq,
275 &e.prev_hash_hex,
276 &e.kind,
277 &e.payload_json,
278 &e.signer_pubkey_hex,
279 e.time_unix,
280 );
281 e.entry_hash_hex = hex::encode(hash);
282 e.signature_hex = hex::encode(attacker.sign(&hash).to_bytes()); assert_eq!(led.verify(), Err(LedgerTamper::BadSignature { seq: 0 }));
284 }
285
286 #[test]
287 fn serde_round_trips_and_still_verifies() {
288 let sk = signer(3);
289 let mut led = AccountabilityLedger::new();
290 led.append("switch", r#"{"fired":true}"#, &sk, 1_000);
291 let json = serde_json::to_string(&led).unwrap();
292 let back: AccountabilityLedger = serde_json::from_str(&json).unwrap();
293 assert_eq!(led, back);
294 assert_eq!(back.verify(), Ok(()));
295 }
296}