mur_common/trust/
rotation.rs1use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct RotationManifest {
9 pub old_pubkey: String,
10 pub new_pubkey: String,
11 pub issued_at: String,
12 pub sig_old: String,
13 pub sig_new: String,
14}
15
16impl RotationManifest {
17 pub fn verify(&self) -> Result<(), String> {
19 use base64::{Engine, engine::general_purpose::STANDARD as B64};
20 use ed25519_dalek::{Signature, VerifyingKey};
21
22 let message = format!("{}{}{}", self.old_pubkey, self.new_pubkey, self.issued_at);
23 let msg_bytes = message.as_bytes();
24
25 let old_pk_bytes: [u8; 32] = B64
26 .decode(&self.old_pubkey)
27 .map_err(|e| format!("old_pubkey b64: {e}"))?
28 .try_into()
29 .map_err(|_| "old_pubkey not 32 bytes".to_string())?;
30 let old_vk = VerifyingKey::from_bytes(&old_pk_bytes)
31 .map_err(|e| format!("old_pubkey decode: {e}"))?;
32 let old_sig_bytes: [u8; 64] = B64
33 .decode(&self.sig_old)
34 .map_err(|e| format!("sig_old b64: {e}"))?
35 .try_into()
36 .map_err(|_| "sig_old not 64 bytes".to_string())?;
37 let old_sig = Signature::from_bytes(&old_sig_bytes);
38 old_vk
39 .verify_strict(msg_bytes, &old_sig)
40 .map_err(|e| format!("old key signature: {e}"))?;
41
42 let new_pk_bytes: [u8; 32] = B64
43 .decode(&self.new_pubkey)
44 .map_err(|e| format!("new_pubkey b64: {e}"))?
45 .try_into()
46 .map_err(|_| "new_pubkey not 32 bytes".to_string())?;
47 let new_vk = VerifyingKey::from_bytes(&new_pk_bytes)
48 .map_err(|e| format!("new_pubkey decode: {e}"))?;
49 let new_sig_bytes: [u8; 64] = B64
50 .decode(&self.sig_new)
51 .map_err(|e| format!("sig_new b64: {e}"))?
52 .try_into()
53 .map_err(|_| "sig_new not 64 bytes".to_string())?;
54 let new_sig = Signature::from_bytes(&new_sig_bytes);
55 new_vk
56 .verify_strict(msg_bytes, &new_sig)
57 .map_err(|e| format!("new key signature: {e}"))?;
58
59 Ok(())
60 }
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66 use base64::{Engine, engine::general_purpose::STANDARD as B64};
67 use ed25519_dalek::{Signer, SigningKey};
68
69 fn make_signed(old: &SigningKey, new: &SigningKey, issued_at: &str) -> RotationManifest {
70 let old_pk = B64.encode(old.verifying_key().as_bytes());
71 let new_pk = B64.encode(new.verifying_key().as_bytes());
72 let msg = format!("{}{}{}", old_pk, new_pk, issued_at);
73 let sig_old = B64.encode(old.sign(msg.as_bytes()).to_bytes());
74 let sig_new = B64.encode(new.sign(msg.as_bytes()).to_bytes());
75 RotationManifest {
76 old_pubkey: old_pk,
77 new_pubkey: new_pk,
78 issued_at: issued_at.to_string(),
79 sig_old,
80 sig_new,
81 }
82 }
83
84 #[test]
85 fn valid_rotation_verifies() {
86 let old = SigningKey::from_bytes(&[1u8; 32]);
87 let new = SigningKey::from_bytes(&[2u8; 32]);
88 let manifest = make_signed(&old, &new, "2026-05-20T12:00:00Z");
89 manifest.verify().unwrap();
90 }
91
92 #[test]
93 fn tampered_timestamp_rejected() {
94 let old = SigningKey::from_bytes(&[1u8; 32]);
95 let new = SigningKey::from_bytes(&[2u8; 32]);
96 let mut manifest = make_signed(&old, &new, "2026-05-20T12:00:00Z");
97 manifest.issued_at = "2025-01-01T00:00:00Z".into();
98 assert!(manifest.verify().is_err());
99 }
100
101 #[test]
102 fn missing_new_key_signature_rejected() {
103 let old = SigningKey::from_bytes(&[1u8; 32]);
104 let new = SigningKey::from_bytes(&[2u8; 32]);
105 let attacker = SigningKey::from_bytes(&[99u8; 32]);
106 let mut manifest = make_signed(&old, &new, "2026-05-20T12:00:00Z");
108 let attacker_sig = attacker.sign(
109 format!(
110 "{}{}{}",
111 manifest.old_pubkey, manifest.new_pubkey, manifest.issued_at
112 )
113 .as_bytes(),
114 );
115 manifest.sig_new = B64.encode(attacker_sig.to_bytes());
116 assert!(manifest.verify().is_err());
117 }
118}