mur_common/
snapshot_request.rs1use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10
11use crate::identity::{AgentIdentity, verify_bytes};
12
13pub const SNAPSHOT_REQUEST_DIR: &str = "inbox/snapshot-requests";
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct SnapshotRequest {
18 pub agent: String,
19 pub requested_at: DateTime<Utc>,
20 #[serde(default)]
23 pub key_version: u32,
24 pub sig: String,
26}
27
28fn sign_input(agent: &str, requested_at: &DateTime<Utc>, key_version: u32) -> Vec<u8> {
30 format!(
31 "mur-snapshot-request-v1\n{agent}\n{}\n{key_version}",
32 requested_at.to_rfc3339()
33 )
34 .into_bytes()
35}
36
37impl SnapshotRequest {
38 pub fn create(agent: &str, identity: &AgentIdentity, now: DateTime<Utc>) -> Self {
39 let input = sign_input(agent, &now, 0);
40 Self {
41 agent: agent.to_string(),
42 requested_at: now,
43 key_version: 0,
44 sig: identity.sign_multibase(&input),
45 }
46 }
47
48 pub fn verify(&self, pubkey: &[u8; 32]) -> bool {
50 let input = sign_input(&self.agent, &self.requested_at, self.key_version);
51 verify_bytes(pubkey, &input, &self.sig)
52 }
53
54 pub fn is_fresh(&self, now: DateTime<Utc>, max_age_secs: u64) -> bool {
57 let age = now.signed_duration_since(self.requested_at);
58 age >= chrono::Duration::zero() && age <= chrono::Duration::seconds(max_age_secs as i64)
59 }
60}
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65
66 fn identity() -> AgentIdentity {
67 AgentIdentity::generate()
68 }
69
70 #[test]
71 fn sign_verify_roundtrip() {
72 let id = identity();
73 let req = SnapshotRequest::create("dr_worker_4", &id, Utc::now());
74 assert!(req.verify(&id.verifying_key_bytes()));
75 }
76
77 #[test]
78 fn tampered_agent_name_fails_verification() {
79 let id = identity();
80 let mut req = SnapshotRequest::create("dr_worker_4", &id, Utc::now());
81 req.agent = "dr_worker_1".into(); assert!(!req.verify(&id.verifying_key_bytes()));
83 }
84
85 #[test]
86 fn wrong_key_fails_verification() {
87 let req = SnapshotRequest::create("a", &identity(), Utc::now());
88 assert!(!req.verify(&identity().verifying_key_bytes()));
89 }
90
91 #[test]
92 fn freshness_window_rejects_old_and_future() {
93 let id = identity();
94 let now = Utc::now();
95 let req = SnapshotRequest::create("a", &id, now);
96 assert!(req.is_fresh(now, 600));
97 assert!(!req.is_fresh(now + chrono::Duration::seconds(601), 600)); assert!(!req.is_fresh(now - chrono::Duration::seconds(1), 600)); }
100
101 #[test]
102 fn yaml_roundtrip_preserves_signature() {
103 let id = identity();
104 let req = SnapshotRequest::create("a", &id, Utc::now());
105 let yaml = serde_yaml_ng::to_string(&req).unwrap();
106 let back: SnapshotRequest = serde_yaml_ng::from_str(&yaml).unwrap();
107 assert!(back.verify(&id.verifying_key_bytes()));
108 }
109}