use base64::Engine as _;
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use sha2::{Digest, Sha256};
use super::event::{
canonical_event_bytes, canonical_json_bytes, EventId, EventSignature, SessionEventKind,
StoredEvent,
};
pub const ALGORITHM: &str = "ed25519";
#[derive(Clone)]
pub struct SessionSigner {
inner: std::sync::Arc<SigningKey>,
key_id: String,
}
impl SessionSigner {
pub fn from_seed(seed: [u8; 32]) -> Self {
let key = SigningKey::from_bytes(&seed);
let key_id = key_id_for(&key.verifying_key());
Self {
inner: std::sync::Arc::new(key),
key_id,
}
}
pub fn key_id(&self) -> &str {
&self.key_id
}
pub fn verifying_key(&self) -> VerifyingKey {
self.inner.verifying_key()
}
pub fn sign_event(&self, event: &StoredEvent) -> EventSignature {
let bytes = canonical_event_bytes(event);
sign_bytes(&self.inner, &self.key_id, &bytes)
}
pub fn sign_receipt(&self, receipt_root_hash: &str) -> EventSignature {
let material = receipt_signing_material(receipt_root_hash);
sign_bytes(&self.inner, &self.key_id, material.as_bytes())
}
}
fn sign_bytes(key: &SigningKey, key_id: &str, bytes: &[u8]) -> EventSignature {
let signature: Signature = key.sign(bytes);
EventSignature {
algorithm: ALGORITHM.to_string(),
key_id: key_id.to_string(),
signature: base64::engine::general_purpose::STANDARD.encode(signature.to_bytes()),
}
}
fn receipt_signing_material(receipt_root_hash: &str) -> String {
format!("harn.session.receipt.v1\nroot={receipt_root_hash}\n")
}
pub fn key_id_for(verifying_key: &VerifyingKey) -> String {
let mut hasher = Sha256::new();
hasher.update(verifying_key.as_bytes());
let digest = hasher.finalize();
format!("sha256:{}", &hex::encode(digest)[..32])
}
pub fn compute_record_hash(event: &StoredEvent) -> String {
let mut hasher = Sha256::new();
hasher.update(canonical_event_bytes(event));
finalize_sha256(hasher)
}
fn finalize_sha256(hasher: Sha256) -> String {
format!("sha256:{}", hex::encode(hasher.finalize()))
}
#[derive(Debug)]
pub enum VerifyError {
NotSigned,
UnsupportedAlgorithm(String),
DecodeError(String),
InvalidShape(String),
BadSignature,
HashMismatch { stored: String, computed: String },
}
impl std::fmt::Display for VerifyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotSigned => write!(f, "event is not signed"),
Self::UnsupportedAlgorithm(algorithm) => {
write!(f, "unsupported signature algorithm '{algorithm}'")
}
Self::DecodeError(message) => {
write!(f, "signature base64 decode failed: {message}")
}
Self::InvalidShape(message) => write!(f, "signature shape invalid: {message}"),
Self::BadSignature => write!(f, "signature did not verify against the key"),
Self::HashMismatch { stored, computed } => {
write!(
f,
"record_hash mismatch: stored '{stored}' vs computed '{computed}'"
)
}
}
}
}
impl std::error::Error for VerifyError {}
pub fn verify_event(event: &StoredEvent, verifying_key: &VerifyingKey) -> Result<(), VerifyError> {
let computed = compute_record_hash(event);
if computed != event.record_hash {
return Err(VerifyError::HashMismatch {
stored: event.record_hash.clone(),
computed,
});
}
let Some(signed_by) = event.signed_by.as_ref() else {
return Err(VerifyError::NotSigned);
};
verify_signature(signed_by, verifying_key, &canonical_event_bytes(event))
}
pub fn verify_receipt_root(
signed_by: &EventSignature,
verifying_key: &VerifyingKey,
receipt_root_hash: &str,
) -> Result<(), VerifyError> {
let material = receipt_signing_material(receipt_root_hash);
verify_signature(signed_by, verifying_key, material.as_bytes())
}
fn verify_signature(
signed_by: &EventSignature,
verifying_key: &VerifyingKey,
bytes: &[u8],
) -> Result<(), VerifyError> {
if signed_by.algorithm != ALGORITHM {
return Err(VerifyError::UnsupportedAlgorithm(
signed_by.algorithm.clone(),
));
}
let signature_bytes = base64::engine::general_purpose::STANDARD
.decode(&signed_by.signature)
.map_err(|error| VerifyError::DecodeError(error.to_string()))?;
let signature = Signature::from_slice(&signature_bytes)
.map_err(|error| VerifyError::InvalidShape(error.to_string()))?;
verifying_key
.verify(bytes, &signature)
.map_err(|_| VerifyError::BadSignature)
}
pub fn verify_event_chain(
events: &[StoredEvent],
event_verifier: Option<&VerifyingKey>,
receipt_verifier: Option<&VerifyingKey>,
) -> (usize, Vec<(EventId, String)>) {
let mut signed = 0usize;
let mut failures: Vec<(EventId, String)> = Vec::new();
for (index, event) in events.iter().enumerate() {
let recomputed = compute_record_hash(event);
if recomputed != event.record_hash {
failures.push((
event.event_id,
format!(
"record_hash mismatch: stored '{stored}' vs computed '{recomputed}'",
stored = event.record_hash
),
));
continue;
}
let Some(signed_by) = event.signed_by.as_ref() else {
continue;
};
let is_receipt = matches!(event.kind, SessionEventKind::Receipt);
let verifier = if is_receipt {
receipt_verifier
} else {
event_verifier
};
let Some(verifier) = verifier else {
signed += 1;
continue;
};
let result = if is_receipt {
let pre_receipt_root = chain_root_hash(&events[..index]);
verify_receipt_root(signed_by, verifier, &pre_receipt_root)
} else {
verify_event(event, verifier)
};
match result {
Ok(()) => signed += 1,
Err(error) => failures.push((event.event_id, error.to_string())),
}
}
(signed, failures)
}
pub fn chain_root_init() -> String {
let mut hasher = Sha256::new();
hasher.update(b"harn.session.chain.v2");
finalize_sha256(hasher)
}
pub fn chain_root_fold(prev_root: &str, record_hash: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(prev_root.as_bytes());
hasher.update(b"\n");
hasher.update(record_hash.as_bytes());
finalize_sha256(hasher)
}
pub fn chain_root_hash(events: &[StoredEvent]) -> String {
events.iter().fold(chain_root_init(), |root, event| {
chain_root_fold(&root, &event.record_hash)
})
}
pub fn re_anchor_events(events: &[StoredEvent], new_session_id: &str) -> Vec<StoredEvent> {
let mut rewritten = Vec::with_capacity(events.len());
let mut prev_hash: Option<String> = None;
for event in events {
let mut copied = event.clone();
copied.session_id = new_session_id.to_string();
copied.prev_hash = prev_hash.clone();
copied.record_hash = compute_record_hash(&copied);
copied.signed_by = None;
prev_hash = Some(copied.record_hash.clone());
rewritten.push(copied);
}
rewritten
}
pub fn canonical_receipt_payload(
session_id: &str,
last_event_id: super::event::EventId,
chain_root: &str,
) -> serde_json::Value {
serde_json::json!({
"schema": "harn.session.receipt.v1",
"session_id": session_id,
"last_event_id": last_event_id,
"chain_root": chain_root,
})
}
pub fn canonical_value_hash(value: &serde_json::Value) -> String {
let mut hasher = Sha256::new();
hasher.update(canonical_json_bytes(value));
finalize_sha256(hasher)
}