1use base64::Engine as _;
17use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
18use sha2::{Digest, Sha256};
19
20use super::event::{
21 canonical_event_bytes, canonical_json_bytes, EventId, EventSignature, SessionEventKind,
22 StoredEvent,
23};
24use super::store::{SessionMeta, VerifyFailure, VerifyReport};
25
26pub const ALGORITHM: &str = "ed25519";
27
28#[derive(Clone)]
29pub struct SessionSigner {
30 inner: std::sync::Arc<SigningKey>,
31 key_id: String,
32}
33
34impl SessionSigner {
35 pub fn from_seed(seed: [u8; 32]) -> Self {
39 let key = SigningKey::from_bytes(&seed);
40 let key_id = key_id_for(&key.verifying_key());
41 Self {
42 inner: std::sync::Arc::new(key),
43 key_id,
44 }
45 }
46
47 pub fn key_id(&self) -> &str {
48 &self.key_id
49 }
50
51 pub fn verifying_key(&self) -> VerifyingKey {
52 self.inner.verifying_key()
53 }
54
55 pub fn sign_event(&self, event: &StoredEvent) -> EventSignature {
56 let bytes = canonical_event_bytes(event);
57 sign_bytes(&self.inner, &self.key_id, &bytes)
58 }
59
60 pub fn sign_receipt(&self, receipt_root_hash: &str) -> EventSignature {
64 let material = receipt_signing_material(receipt_root_hash);
65 sign_bytes(&self.inner, &self.key_id, material.as_bytes())
66 }
67}
68
69fn sign_bytes(key: &SigningKey, key_id: &str, bytes: &[u8]) -> EventSignature {
70 let signature: Signature = key.sign(bytes);
71 EventSignature {
72 algorithm: ALGORITHM.to_string(),
73 key_id: key_id.to_string(),
74 signature: base64::engine::general_purpose::STANDARD.encode(signature.to_bytes()),
75 }
76}
77
78fn receipt_signing_material(receipt_root_hash: &str) -> String {
79 format!("harn.session.receipt.v1\nroot={receipt_root_hash}\n")
80}
81
82pub fn key_id_for(verifying_key: &VerifyingKey) -> String {
83 let mut hasher = Sha256::new();
84 hasher.update(verifying_key.as_bytes());
85 let digest = hasher.finalize();
86 format!("sha256:{}", &hex::encode(digest)[..32])
87}
88
89pub fn compute_record_hash(event: &StoredEvent) -> String {
92 let mut hasher = Sha256::new();
93 hasher.update(canonical_event_bytes(event));
94 finalize_sha256(hasher)
95}
96
97fn finalize_sha256(hasher: Sha256) -> String {
102 format!("sha256:{}", hex::encode(hasher.finalize()))
103}
104
105#[derive(Debug)]
107pub enum VerifyError {
108 NotSigned,
109 UnsupportedAlgorithm(String),
110 DecodeError(String),
111 InvalidShape(String),
112 BadSignature,
113 HashMismatch { stored: String, computed: String },
114}
115
116impl std::fmt::Display for VerifyError {
117 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118 match self {
119 Self::NotSigned => write!(f, "event is not signed"),
120 Self::UnsupportedAlgorithm(algorithm) => {
121 write!(f, "unsupported signature algorithm '{algorithm}'")
122 }
123 Self::DecodeError(message) => {
124 write!(f, "signature base64 decode failed: {message}")
125 }
126 Self::InvalidShape(message) => write!(f, "signature shape invalid: {message}"),
127 Self::BadSignature => write!(f, "signature did not verify against the key"),
128 Self::HashMismatch { stored, computed } => {
129 write!(
130 f,
131 "record_hash mismatch: stored '{stored}' vs computed '{computed}'"
132 )
133 }
134 }
135 }
136}
137
138impl std::error::Error for VerifyError {}
139
140pub fn verify_event(event: &StoredEvent, verifying_key: &VerifyingKey) -> Result<(), VerifyError> {
141 if event.is_redacted_projection() {
142 return Err(VerifyError::InvalidShape(format!(
143 "redacted projection cannot authenticate canonical source hash '{}'",
144 event.source_record_hash()
145 )));
146 }
147 let computed = compute_record_hash(event);
148 if computed != event.record_hash {
149 return Err(VerifyError::HashMismatch {
150 stored: event.record_hash.clone(),
151 computed,
152 });
153 }
154 let Some(signed_by) = event.signed_by.as_ref() else {
155 return Err(VerifyError::NotSigned);
156 };
157 verify_signature(signed_by, verifying_key, &canonical_event_bytes(event))
158}
159
160pub fn verify_receipt_root(
161 signed_by: &EventSignature,
162 verifying_key: &VerifyingKey,
163 receipt_root_hash: &str,
164) -> Result<(), VerifyError> {
165 let material = receipt_signing_material(receipt_root_hash);
166 verify_signature(signed_by, verifying_key, material.as_bytes())
167}
168
169fn verify_signature(
170 signed_by: &EventSignature,
171 verifying_key: &VerifyingKey,
172 bytes: &[u8],
173) -> Result<(), VerifyError> {
174 if signed_by.algorithm != ALGORITHM {
175 return Err(VerifyError::UnsupportedAlgorithm(
176 signed_by.algorithm.clone(),
177 ));
178 }
179 let signature_bytes = base64::engine::general_purpose::STANDARD
180 .decode(&signed_by.signature)
181 .map_err(|error| VerifyError::DecodeError(error.to_string()))?;
182 let signature = Signature::from_slice(&signature_bytes)
183 .map_err(|error| VerifyError::InvalidShape(error.to_string()))?;
184 verifying_key
185 .verify(bytes, &signature)
186 .map_err(|_| VerifyError::BadSignature)
187}
188
189pub fn verify_event_chain(
208 events: &[StoredEvent],
209 event_verifier: Option<&VerifyingKey>,
210 receipt_verifier: Option<&VerifyingKey>,
211) -> (usize, Vec<(EventId, String)>) {
212 let (signed, failures) = verify_event_chain_detailed(events, event_verifier, receipt_verifier);
213 (
214 signed,
215 failures
216 .into_iter()
217 .map(|failure| (failure.event_id, failure.reason))
218 .collect(),
219 )
220}
221
222struct ChainFailure {
223 event_id: EventId,
224 reason: String,
225}
226
227fn verify_event_chain_detailed(
228 events: &[StoredEvent],
229 event_verifier: Option<&VerifyingKey>,
230 receipt_verifier: Option<&VerifyingKey>,
231) -> (usize, Vec<ChainFailure>) {
232 let mut signed = 0usize;
233 let mut failures = Vec::new();
234 let mut expected_prev_hash: Option<&str> = None;
235 for (index, event) in events.iter().enumerate() {
236 let expected_event_id = index as EventId + 1;
237 if event.event_id != expected_event_id {
238 failures.push(ChainFailure {
239 event_id: event.event_id,
240 reason: format!(
241 "event_id sequence gap: expected {expected_event_id}, found {}",
242 event.event_id
243 ),
244 });
245 }
246 if event.prev_hash.as_deref() != expected_prev_hash {
247 failures.push(ChainFailure {
248 event_id: event.event_id,
249 reason: "prev_hash chain break".to_string(),
250 });
251 }
252 if event.is_redacted_projection() {
253 failures.push(ChainFailure {
254 event_id: event.event_id,
255 reason: format!(
256 "redacted projection cannot authenticate canonical source hash '{}'",
257 event.source_record_hash()
258 ),
259 });
260 expected_prev_hash = Some(event.source_record_hash());
261 continue;
262 }
263 let recomputed = compute_record_hash(event);
264 if recomputed != event.record_hash {
265 failures.push(ChainFailure {
266 event_id: event.event_id,
267 reason: format!(
268 "record_hash mismatch: stored '{stored}' vs computed '{recomputed}'",
269 stored = event.record_hash
270 ),
271 });
272 expected_prev_hash = Some(event.record_hash.as_str());
273 continue;
274 }
275 expected_prev_hash = Some(event.record_hash.as_str());
276 let Some(signed_by) = event.signed_by.as_ref() else {
277 continue;
278 };
279 let is_receipt = matches!(event.kind, SessionEventKind::Receipt);
280 let verifier = if is_receipt {
281 receipt_verifier
282 } else {
283 event_verifier
284 };
285 let Some(verifier) = verifier else {
286 signed += 1;
287 continue;
288 };
289 let result = if is_receipt {
290 let pre_receipt_root = chain_root_hash(&events[..index]);
291 verify_receipt_root(signed_by, verifier, &pre_receipt_root)
292 } else {
293 verify_event(event, verifier)
294 };
295 match result {
296 Ok(()) => signed += 1,
297 Err(error) => failures.push(ChainFailure {
298 event_id: event.event_id,
299 reason: error.to_string(),
300 }),
301 }
302 }
303 (signed, failures)
304}
305
306pub fn verify_session_chain(
309 meta: &SessionMeta,
310 events: &[StoredEvent],
311 event_verifier: Option<&VerifyingKey>,
312 receipt_verifier: Option<&VerifyingKey>,
313) -> VerifyReport {
314 let chain_root = chain_root_hash(events);
315 let (signed_event_count, mut failures) =
316 verify_event_chain_detailed(events, event_verifier, receipt_verifier);
317 let tail_id = events.last().map(|event| event.event_id).unwrap_or(0);
318 if meta.event_count != events.len() {
319 failures.push(ChainFailure {
320 event_id: tail_id,
321 reason: format!(
322 "session event_count mismatch: stored {}, found {}",
323 meta.event_count,
324 events.len()
325 ),
326 });
327 }
328 if meta.last_event_id != events.last().map(|event| event.event_id) {
329 failures.push(ChainFailure {
330 event_id: tail_id,
331 reason: "session last_event_id mismatch".to_string(),
332 });
333 }
334 let expected_root = if events.is_empty() {
335 None
336 } else {
337 Some(chain_root.as_str())
338 };
339 if meta.chain_root_hash.as_deref() != expected_root {
340 failures.push(ChainFailure {
341 event_id: tail_id,
342 reason: "session chain_root_hash mismatch".to_string(),
343 });
344 }
345 VerifyReport {
346 session_id: meta.id.clone(),
347 chain_root_hash: chain_root,
348 event_count: events.len(),
349 signed_event_count,
350 failures: failures
351 .into_iter()
352 .map(|failure| VerifyFailure {
353 event_id: failure.event_id,
354 reason: failure.reason,
355 })
356 .collect(),
357 }
358}
359
360pub fn chain_root_init() -> String {
364 let mut hasher = Sha256::new();
365 hasher.update(b"harn.session.chain.v2");
366 finalize_sha256(hasher)
367}
368
369pub fn chain_root_fold(prev_root: &str, record_hash: &str) -> String {
373 let mut hasher = Sha256::new();
374 hasher.update(prev_root.as_bytes());
375 hasher.update(b"\n");
376 hasher.update(record_hash.as_bytes());
377 finalize_sha256(hasher)
378}
379
380pub fn chain_root_hash(events: &[StoredEvent]) -> String {
386 events.iter().fold(chain_root_init(), |root, event| {
387 chain_root_fold(&root, event.source_record_hash())
388 })
389}
390
391pub fn re_anchor_events(events: &[StoredEvent], new_session_id: &str) -> Vec<StoredEvent> {
398 let mut rewritten = Vec::with_capacity(events.len());
399 let mut prev_hash: Option<String> = None;
400 for event in events {
401 let mut copied = event.clone();
402 copied.session_id = new_session_id.to_string();
403 copied.prev_hash = prev_hash.clone();
404 copied.record_hash = compute_record_hash(&copied);
405 copied.signed_by = None;
411 prev_hash = Some(copied.record_hash.clone());
412 rewritten.push(copied);
413 }
414 rewritten
415}
416
417pub fn canonical_receipt_payload(
419 session_id: &str,
420 last_event_id: super::event::EventId,
421 chain_root: &str,
422) -> serde_json::Value {
423 serde_json::json!({
424 "schema": "harn.session.receipt.v1",
425 "session_id": session_id,
426 "last_event_id": last_event_id,
427 "chain_root": chain_root,
428 })
429}
430
431pub fn canonical_value_hash(value: &serde_json::Value) -> String {
435 let mut hasher = Sha256::new();
436 hasher.update(canonical_json_bytes(value));
437 finalize_sha256(hasher)
438}