use crate::types::{AgentId, TenantId};
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Clone, Debug, Serialize, Deserialize)]
#[non_exhaustive]
pub struct MerklePrefixProof {
pub root: String,
pub at_seq: Option<u64>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[non_exhaustive]
pub struct EnvelopeSignature {
pub signature_hex: String,
pub verifying_key_hex: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[non_exhaustive]
pub struct HandoffEnvelope {
pub from_run: String,
pub merkle_prefix_proof: MerklePrefixProof,
pub redacted_state: Vec<u8>,
pub source_signature: EnvelopeSignature,
pub source_identity: AgentId,
pub tenant: Option<TenantId>,
pub issued_at: String,
pub expires_at: String,
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct HandoffProof {
pub from_run: String,
pub source_identity: AgentId,
pub tenant: Option<TenantId>,
pub redacted_state: Vec<u8>,
}
#[derive(Debug)]
pub(crate) struct CanonicalPayload {
pub from_run: String,
pub merkle_root: String,
pub state_hash: String,
pub source_identity: String,
pub tenant: Option<String>,
pub issued_at: String,
pub expires_at: String,
}
impl CanonicalPayload {
pub(crate) fn from_envelope(env: &HandoffEnvelope) -> Self {
let state_hash = blake3::hash(&env.redacted_state).to_hex().to_string();
Self {
from_run: env.from_run.clone(),
merkle_root: env.merkle_prefix_proof.root.clone(),
state_hash,
source_identity: env.source_identity.0.clone(),
tenant: env.tenant.as_ref().map(|t| t.0.clone()),
issued_at: env.issued_at.clone(),
expires_at: env.expires_at.clone(),
}
}
pub(crate) fn to_bytes(&self) -> Vec<u8> {
let mut map: BTreeMap<&str, serde_json::Value> = BTreeMap::new();
map.insert("expires_at", self.expires_at.clone().into());
map.insert("from_run", self.from_run.clone().into());
map.insert("issued_at", self.issued_at.clone().into());
map.insert("merkle_root", self.merkle_root.clone().into());
map.insert("source_identity", self.source_identity.clone().into());
map.insert("state_hash", self.state_hash.clone().into());
map.insert(
"tenant",
match &self.tenant {
Some(t) => t.clone().into(),
None => serde_json::Value::Null,
},
);
serde_json::to_vec(&map).expect("BTreeMap<&str, Value> is always JSON-serialisable")
}
}
pub(crate) fn verify_signature(env: &HandoffEnvelope) -> Result<(), String> {
let sig_bytes = hex::decode(&env.source_signature.signature_hex)
.map_err(|e| format!("signature_hex decode: {e}"))?;
let sig = Signature::from_slice(&sig_bytes).map_err(|e| format!("signature parse: {e}"))?;
let vk_bytes = hex::decode(&env.source_signature.verifying_key_hex)
.map_err(|e| format!("verifying_key_hex decode: {e}"))?;
let vk_array: [u8; 32] = vk_bytes
.try_into()
.map_err(|_| "verifying key must be exactly 32 bytes".to_string())?;
let vk =
VerifyingKey::from_bytes(&vk_array).map_err(|e| format!("verifying key parse: {e}"))?;
let canonical_bytes = CanonicalPayload::from_envelope(env).to_bytes();
vk.verify(&canonical_bytes, &sig)
.map_err(|e| format!("signature math invalid: {e}"))?;
Ok(())
}