use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::identity::{AgentIdentity, verify_bytes};
pub const SNAPSHOT_REQUEST_DIR: &str = "inbox/snapshot-requests";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SnapshotRequest {
pub agent: String,
pub requested_at: DateTime<Utc>,
#[serde(default)]
pub key_version: u32,
pub sig: String,
}
fn sign_input(agent: &str, requested_at: &DateTime<Utc>, key_version: u32) -> Vec<u8> {
format!(
"mur-snapshot-request-v1\n{agent}\n{}\n{key_version}",
requested_at.to_rfc3339()
)
.into_bytes()
}
impl SnapshotRequest {
pub fn create(agent: &str, identity: &AgentIdentity, now: DateTime<Utc>) -> Self {
let input = sign_input(agent, &now, 0);
Self {
agent: agent.to_string(),
requested_at: now,
key_version: 0,
sig: identity.sign_multibase(&input),
}
}
pub fn verify(&self, pubkey: &[u8; 32]) -> bool {
let input = sign_input(&self.agent, &self.requested_at, self.key_version);
verify_bytes(pubkey, &input, &self.sig)
}
pub fn is_fresh(&self, now: DateTime<Utc>, max_age_secs: u64) -> bool {
let age = now.signed_duration_since(self.requested_at);
age >= chrono::Duration::zero() && age <= chrono::Duration::seconds(max_age_secs as i64)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn identity() -> AgentIdentity {
AgentIdentity::generate()
}
#[test]
fn sign_verify_roundtrip() {
let id = identity();
let req = SnapshotRequest::create("dr_worker_4", &id, Utc::now());
assert!(req.verify(&id.verifying_key_bytes()));
}
#[test]
fn tampered_agent_name_fails_verification() {
let id = identity();
let mut req = SnapshotRequest::create("dr_worker_4", &id, Utc::now());
req.agent = "dr_worker_1".into(); assert!(!req.verify(&id.verifying_key_bytes()));
}
#[test]
fn wrong_key_fails_verification() {
let req = SnapshotRequest::create("a", &identity(), Utc::now());
assert!(!req.verify(&identity().verifying_key_bytes()));
}
#[test]
fn freshness_window_rejects_old_and_future() {
let id = identity();
let now = Utc::now();
let req = SnapshotRequest::create("a", &id, now);
assert!(req.is_fresh(now, 600));
assert!(!req.is_fresh(now + chrono::Duration::seconds(601), 600)); assert!(!req.is_fresh(now - chrono::Duration::seconds(1), 600)); }
#[test]
fn yaml_roundtrip_preserves_signature() {
let id = identity();
let req = SnapshotRequest::create("a", &id, Utc::now());
let yaml = serde_yaml_ng::to_string(&req).unwrap();
let back: SnapshotRequest = serde_yaml_ng::from_str(&yaml).unwrap();
assert!(back.verify(&id.verifying_key_bytes()));
}
}