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};
24
25pub const ALGORITHM: &str = "ed25519";
26
27#[derive(Clone)]
28pub struct SessionSigner {
29 inner: std::sync::Arc<SigningKey>,
30 key_id: String,
31}
32
33impl SessionSigner {
34 pub fn from_seed(seed: [u8; 32]) -> Self {
38 let key = SigningKey::from_bytes(&seed);
39 let key_id = key_id_for(&key.verifying_key());
40 Self {
41 inner: std::sync::Arc::new(key),
42 key_id,
43 }
44 }
45
46 pub fn key_id(&self) -> &str {
47 &self.key_id
48 }
49
50 pub fn verifying_key(&self) -> VerifyingKey {
51 self.inner.verifying_key()
52 }
53
54 pub fn sign_event(&self, event: &StoredEvent) -> EventSignature {
55 let bytes = canonical_event_bytes(event);
56 sign_bytes(&self.inner, &self.key_id, &bytes)
57 }
58
59 pub fn sign_receipt(&self, receipt_root_hash: &str) -> EventSignature {
63 let material = receipt_signing_material(receipt_root_hash);
64 sign_bytes(&self.inner, &self.key_id, material.as_bytes())
65 }
66}
67
68fn sign_bytes(key: &SigningKey, key_id: &str, bytes: &[u8]) -> EventSignature {
69 let signature: Signature = key.sign(bytes);
70 EventSignature {
71 algorithm: ALGORITHM.to_string(),
72 key_id: key_id.to_string(),
73 signature: base64::engine::general_purpose::STANDARD.encode(signature.to_bytes()),
74 }
75}
76
77fn receipt_signing_material(receipt_root_hash: &str) -> String {
78 format!("harn.session.receipt.v1\nroot={receipt_root_hash}\n")
79}
80
81pub fn key_id_for(verifying_key: &VerifyingKey) -> String {
82 let mut hasher = Sha256::new();
83 hasher.update(verifying_key.as_bytes());
84 let digest = hasher.finalize();
85 format!("sha256:{}", &hex::encode(digest)[..32])
86}
87
88pub fn compute_record_hash(event: &StoredEvent) -> String {
91 let mut hasher = Sha256::new();
92 hasher.update(canonical_event_bytes(event));
93 finalize_sha256(hasher)
94}
95
96fn finalize_sha256(hasher: Sha256) -> String {
101 format!("sha256:{}", hex::encode(hasher.finalize()))
102}
103
104#[derive(Debug)]
106pub enum VerifyError {
107 NotSigned,
108 UnsupportedAlgorithm(String),
109 DecodeError(String),
110 InvalidShape(String),
111 BadSignature,
112 HashMismatch { stored: String, computed: String },
113}
114
115impl std::fmt::Display for VerifyError {
116 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117 match self {
118 Self::NotSigned => write!(f, "event is not signed"),
119 Self::UnsupportedAlgorithm(algorithm) => {
120 write!(f, "unsupported signature algorithm '{algorithm}'")
121 }
122 Self::DecodeError(message) => {
123 write!(f, "signature base64 decode failed: {message}")
124 }
125 Self::InvalidShape(message) => write!(f, "signature shape invalid: {message}"),
126 Self::BadSignature => write!(f, "signature did not verify against the key"),
127 Self::HashMismatch { stored, computed } => {
128 write!(
129 f,
130 "record_hash mismatch: stored '{stored}' vs computed '{computed}'"
131 )
132 }
133 }
134 }
135}
136
137impl std::error::Error for VerifyError {}
138
139pub fn verify_event(event: &StoredEvent, verifying_key: &VerifyingKey) -> Result<(), VerifyError> {
140 let computed = compute_record_hash(event);
141 if computed != event.record_hash {
142 return Err(VerifyError::HashMismatch {
143 stored: event.record_hash.clone(),
144 computed,
145 });
146 }
147 let Some(signed_by) = event.signed_by.as_ref() else {
148 return Err(VerifyError::NotSigned);
149 };
150 verify_signature(signed_by, verifying_key, &canonical_event_bytes(event))
151}
152
153pub fn verify_receipt_root(
154 signed_by: &EventSignature,
155 verifying_key: &VerifyingKey,
156 receipt_root_hash: &str,
157) -> Result<(), VerifyError> {
158 let material = receipt_signing_material(receipt_root_hash);
159 verify_signature(signed_by, verifying_key, material.as_bytes())
160}
161
162fn verify_signature(
163 signed_by: &EventSignature,
164 verifying_key: &VerifyingKey,
165 bytes: &[u8],
166) -> Result<(), VerifyError> {
167 if signed_by.algorithm != ALGORITHM {
168 return Err(VerifyError::UnsupportedAlgorithm(
169 signed_by.algorithm.clone(),
170 ));
171 }
172 let signature_bytes = base64::engine::general_purpose::STANDARD
173 .decode(&signed_by.signature)
174 .map_err(|error| VerifyError::DecodeError(error.to_string()))?;
175 let signature = Signature::from_slice(&signature_bytes)
176 .map_err(|error| VerifyError::InvalidShape(error.to_string()))?;
177 verifying_key
178 .verify(bytes, &signature)
179 .map_err(|_| VerifyError::BadSignature)
180}
181
182pub fn verify_event_chain(
201 events: &[StoredEvent],
202 event_verifier: Option<&VerifyingKey>,
203 receipt_verifier: Option<&VerifyingKey>,
204) -> (usize, Vec<(EventId, String)>) {
205 let mut signed = 0usize;
206 let mut failures: Vec<(EventId, String)> = Vec::new();
207 for (index, event) in events.iter().enumerate() {
208 let recomputed = compute_record_hash(event);
209 if recomputed != event.record_hash {
210 failures.push((
211 event.event_id,
212 format!(
213 "record_hash mismatch: stored '{stored}' vs computed '{recomputed}'",
214 stored = event.record_hash
215 ),
216 ));
217 continue;
218 }
219 let Some(signed_by) = event.signed_by.as_ref() else {
220 continue;
221 };
222 let is_receipt = matches!(event.kind, SessionEventKind::Receipt);
223 let verifier = if is_receipt {
224 receipt_verifier
225 } else {
226 event_verifier
227 };
228 let Some(verifier) = verifier else {
229 signed += 1;
230 continue;
231 };
232 let result = if is_receipt {
233 let pre_receipt_root = chain_root_hash(&events[..index]);
234 verify_receipt_root(signed_by, verifier, &pre_receipt_root)
235 } else {
236 verify_event(event, verifier)
237 };
238 match result {
239 Ok(()) => signed += 1,
240 Err(error) => failures.push((event.event_id, error.to_string())),
241 }
242 }
243 (signed, failures)
244}
245
246pub fn chain_root_init() -> String {
250 let mut hasher = Sha256::new();
251 hasher.update(b"harn.session.chain.v2");
252 finalize_sha256(hasher)
253}
254
255pub fn chain_root_fold(prev_root: &str, record_hash: &str) -> String {
259 let mut hasher = Sha256::new();
260 hasher.update(prev_root.as_bytes());
261 hasher.update(b"\n");
262 hasher.update(record_hash.as_bytes());
263 finalize_sha256(hasher)
264}
265
266pub fn chain_root_hash(events: &[StoredEvent]) -> String {
270 events.iter().fold(chain_root_init(), |root, event| {
271 chain_root_fold(&root, &event.record_hash)
272 })
273}
274
275pub fn re_anchor_events(events: &[StoredEvent], new_session_id: &str) -> Vec<StoredEvent> {
282 let mut rewritten = Vec::with_capacity(events.len());
283 let mut prev_hash: Option<String> = None;
284 for event in events {
285 let mut copied = event.clone();
286 copied.session_id = new_session_id.to_string();
287 copied.prev_hash = prev_hash.clone();
288 copied.record_hash = compute_record_hash(&copied);
289 copied.signed_by = None;
295 prev_hash = Some(copied.record_hash.clone());
296 rewritten.push(copied);
297 }
298 rewritten
299}
300
301pub fn canonical_receipt_payload(
303 session_id: &str,
304 last_event_id: super::event::EventId,
305 chain_root: &str,
306) -> serde_json::Value {
307 serde_json::json!({
308 "schema": "harn.session.receipt.v1",
309 "session_id": session_id,
310 "last_event_id": last_event_id,
311 "chain_root": chain_root,
312 })
313}
314
315pub fn canonical_value_hash(value: &serde_json::Value) -> String {
319 let mut hasher = Sha256::new();
320 hasher.update(canonical_json_bytes(value));
321 finalize_sha256(hasher)
322}