1use std::collections::HashMap;
16use std::sync::Mutex;
17
18use chrono::{DateTime, Utc};
19use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
20use rand::rngs::OsRng;
21use serde::{Deserialize, Serialize};
22use tracing::info;
23
24use crate::chain::{ChainEvent, ChainManager};
25use crate::mesh_clock::{Clock, RealClock};
26
27pub const KIND_KEY_ROTATION: &str = "identity.key_rotation";
29
30pub const SOURCE_IDENTITY: &str = "identity";
32
33pub const DEFAULT_GRACE_PERIOD_SECS: u64 = 86_400;
35
36const CANONICAL_PREFIX: &str = "weftos-key-rotation:v1";
38
39#[derive(Debug, thiserror::Error)]
41pub enum KeyRotationError {
42 #[error("no signing key configured on chain manager")]
44 NoSigningKey,
45 #[error("key rotation verification failed: {0}")]
47 Verification(String),
48 #[error("invalid signature encoding: {0}")]
50 BadSignature(String),
51 #[error("invalid public key encoding: {0}")]
53 BadPublicKey(String),
54 #[error("key rotation replay rejected: already applied for this key pair")]
56 Replay,
57 #[error("old key is past grace period and has been revoked")]
59 KeyRevoked,
60 #[error("lock poisoned")]
62 LockPoisoned,
63}
64
65fn bytes_to_hex(data: &[u8]) -> String {
66 data.iter().map(|b| format!("{b:02x}")).collect()
67}
68
69fn hex_to_bytes(s: &str) -> Result<Vec<u8>, KeyRotationError> {
70 if !s.len().is_multiple_of(2) {
71 return Err(KeyRotationError::BadPublicKey(
72 "hex string has odd length".into(),
73 ));
74 }
75 (0..s.len())
76 .step_by(2)
77 .map(|i| {
78 u8::from_str_radix(&s[i..i + 2], 16)
79 .map_err(|e| KeyRotationError::BadPublicKey(e.to_string()))
80 })
81 .collect()
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
86pub struct KeyRotationPayload {
87 pub old_pubkey_hex: String,
89 pub new_pubkey_hex: String,
91 pub old_signature_hex: String,
93 pub new_signature_hex: String,
95 pub grace_period_secs: u64,
98 pub rotated_at_unix: u64,
100}
101
102impl KeyRotationPayload {
103 pub fn canonical_message(
105 old_pubkey: &[u8; 32],
106 new_pubkey: &[u8; 32],
107 grace_period_secs: u64,
108 rotated_at_unix: u64,
109 ) -> Vec<u8> {
110 format!(
111 "{CANONICAL_PREFIX}\n\
112 old_pubkey:{}\n\
113 new_pubkey:{}\n\
114 grace_period_secs:{grace_period_secs}\n\
115 rotated_at:{rotated_at_unix}",
116 bytes_to_hex(old_pubkey),
117 bytes_to_hex(new_pubkey),
118 )
119 .into_bytes()
120 }
121
122 pub fn decode_pubkey(hex_str: &str) -> Result<[u8; 32], KeyRotationError> {
124 let bytes = hex_to_bytes(hex_str)?;
125 if bytes.len() != 32 {
126 return Err(KeyRotationError::BadPublicKey(format!(
127 "expected 32 bytes, got {}",
128 bytes.len()
129 )));
130 }
131 let mut out = [0u8; 32];
132 out.copy_from_slice(&bytes);
133 Ok(out)
134 }
135
136 pub fn decode_signature(hex_str: &str) -> Result<[u8; 64], KeyRotationError> {
138 let bytes = hex_to_bytes(hex_str).map_err(|e| match e {
139 KeyRotationError::BadPublicKey(m) => KeyRotationError::BadSignature(m),
140 other => other,
141 })?;
142 if bytes.len() != 64 {
143 return Err(KeyRotationError::BadSignature(format!(
144 "expected 64 bytes, got {}",
145 bytes.len()
146 )));
147 }
148 let mut out = [0u8; 64];
149 out.copy_from_slice(&bytes);
150 Ok(out)
151 }
152
153 pub fn from_event_payload(
155 payload: &serde_json::Value,
156 ) -> Result<Self, KeyRotationError> {
157 serde_json::from_value(payload.clone()).map_err(|e| {
158 KeyRotationError::Verification(format!("payload deserialize: {e}"))
159 })
160 }
161}
162
163pub fn build_key_rotation_payload(
167 old_key: &SigningKey,
168 new_key: &SigningKey,
169 grace_period_secs: u64,
170 rotated_at_unix: u64,
171) -> KeyRotationPayload {
172 let old_pubkey = old_key.verifying_key().to_bytes();
173 let new_pubkey = new_key.verifying_key().to_bytes();
174 let msg = KeyRotationPayload::canonical_message(
175 &old_pubkey,
176 &new_pubkey,
177 grace_period_secs,
178 rotated_at_unix,
179 );
180 let old_sig = old_key.sign(&msg);
181 let new_sig = new_key.sign(&msg);
182 KeyRotationPayload {
183 old_pubkey_hex: bytes_to_hex(&old_pubkey),
184 new_pubkey_hex: bytes_to_hex(&new_pubkey),
185 old_signature_hex: bytes_to_hex(&old_sig.to_bytes()),
186 new_signature_hex: bytes_to_hex(&new_sig.to_bytes()),
187 grace_period_secs,
188 rotated_at_unix,
189 }
190}
191
192pub fn verify_key_rotation_payload(
197 payload: &KeyRotationPayload,
198) -> Result<(), KeyRotationError> {
199 let old_pk = KeyRotationPayload::decode_pubkey(&payload.old_pubkey_hex)?;
200 let new_pk = KeyRotationPayload::decode_pubkey(&payload.new_pubkey_hex)?;
201 let old_sig_bytes = KeyRotationPayload::decode_signature(&payload.old_signature_hex)?;
202 let new_sig_bytes = KeyRotationPayload::decode_signature(&payload.new_signature_hex)?;
203
204 if old_pk == new_pk {
205 return Err(KeyRotationError::Verification(
206 "old and new public keys must differ".into(),
207 ));
208 }
209
210 let old_vk = VerifyingKey::from_bytes(&old_pk)
211 .map_err(|e| KeyRotationError::BadPublicKey(e.to_string()))?;
212 let new_vk = VerifyingKey::from_bytes(&new_pk)
213 .map_err(|e| KeyRotationError::BadPublicKey(e.to_string()))?;
214
215 let msg = KeyRotationPayload::canonical_message(
216 &old_pk,
217 &new_pk,
218 payload.grace_period_secs,
219 payload.rotated_at_unix,
220 );
221
222 let old_sig = Signature::from_bytes(&old_sig_bytes);
223 let new_sig = Signature::from_bytes(&new_sig_bytes);
224
225 old_vk
226 .verify(&msg, &old_sig)
227 .map_err(|_| KeyRotationError::Verification("old-key signature invalid".into()))?;
228 new_vk
229 .verify(&msg, &new_sig)
230 .map_err(|_| KeyRotationError::Verification("new-key signature invalid".into()))?;
231
232 Ok(())
233}
234
235pub fn verify_key_rotation_event(event: &ChainEvent) -> Result<KeyRotationPayload, KeyRotationError> {
237 if event.kind != KIND_KEY_ROTATION {
238 return Err(KeyRotationError::Verification(format!(
239 "expected kind {KIND_KEY_ROTATION}, got {}",
240 event.kind
241 )));
242 }
243 let payload_val = event
244 .payload
245 .as_ref()
246 .ok_or_else(|| KeyRotationError::Verification("missing payload".into()))?;
247 let payload = KeyRotationPayload::from_event_payload(payload_val)?;
248 verify_key_rotation_payload(&payload)?;
249 Ok(payload)
250}
251
252pub fn rotate_chain_signing_key(
260 chain: &mut ChainManager,
261 grace_period_secs: u64,
262 clock: &dyn Clock,
263) -> Result<(ChainEvent, SigningKey), KeyRotationError> {
264 let old_key = chain
265 .signing_key_clone()
266 .ok_or(KeyRotationError::NoSigningKey)?;
267 let new_key = SigningKey::generate(&mut OsRng);
268 let rotated_at = clock.unix_time_us() / 1_000_000;
269 let payload =
270 build_key_rotation_payload(&old_key, &new_key, grace_period_secs, rotated_at);
271 verify_key_rotation_payload(&payload)?;
273
274 let event = chain.append(
275 SOURCE_IDENTITY,
276 KIND_KEY_ROTATION,
277 Some(serde_json::to_value(&payload).expect("KeyRotationPayload serializes")),
278 );
279 chain.set_signing_key(new_key.clone());
280 info!(
281 sequence = event.sequence,
282 old = %payload.old_pubkey_hex,
283 new = %payload.new_pubkey_hex,
284 "S10 key rotation applied"
285 );
286 Ok((event, new_key))
287}
288
289pub fn rotate_chain_signing_key_now(
291 chain: &mut ChainManager,
292 grace_period_secs: u64,
293) -> Result<(ChainEvent, SigningKey), KeyRotationError> {
294 rotate_chain_signing_key(chain, grace_period_secs, &RealClock)
295}
296
297pub struct KeyRotationVerifier {
303 trusted: Mutex<HashMap<String, Vec<TrustedKey>>>,
305 applied: Mutex<HashMap<String, u64>>,
307 clock: std::sync::Arc<dyn Clock>,
308}
309
310#[derive(Debug, Clone)]
311struct TrustedKey {
312 pubkey: [u8; 32],
313 expires_at_unix: Option<u64>,
316}
317
318impl KeyRotationVerifier {
319 pub fn new(clock: std::sync::Arc<dyn Clock>) -> Self {
321 Self {
322 trusted: Mutex::new(HashMap::new()),
323 applied: Mutex::new(HashMap::new()),
324 clock,
325 }
326 }
327
328 pub fn with_real_clock() -> Self {
330 Self::new(RealClock::shared())
331 }
332
333 pub fn trust_peer(&self, node_id: &str, pubkey: [u8; 32]) -> Result<(), KeyRotationError> {
335 let mut map = self
336 .trusted
337 .lock()
338 .map_err(|_| KeyRotationError::LockPoisoned)?;
339 map.insert(
340 node_id.to_string(),
341 vec![TrustedKey {
342 pubkey,
343 expires_at_unix: None,
344 }],
345 );
346 Ok(())
347 }
348
349 pub fn apply_rotation(
357 &self,
358 node_id: &str,
359 event: &ChainEvent,
360 ) -> Result<KeyRotationPayload, KeyRotationError> {
361 let payload = verify_key_rotation_event(event)?;
362 let old_pk = KeyRotationPayload::decode_pubkey(&payload.old_pubkey_hex)?;
363 let new_pk = KeyRotationPayload::decode_pubkey(&payload.new_pubkey_hex)?;
364
365 let fingerprint = format!(
366 "{}:{}",
367 payload.old_pubkey_hex, payload.new_pubkey_hex
368 );
369 {
370 let applied = self
371 .applied
372 .lock()
373 .map_err(|_| KeyRotationError::LockPoisoned)?;
374 if applied.contains_key(&fingerprint) {
375 return Err(KeyRotationError::Replay);
376 }
377 }
378
379 let now = self.clock.unix_time_us() / 1_000_000;
380 {
381 let mut map = self
382 .trusted
383 .lock()
384 .map_err(|_| KeyRotationError::LockPoisoned)?;
385 let keys = map.entry(node_id.to_string()).or_default();
386
387 keys.retain(|k| match k.expires_at_unix {
389 Some(exp) => now < exp,
390 None => true,
391 });
392 let old_trusted = keys.iter().any(|k| k.pubkey == old_pk);
393 if !old_trusted {
394 if keys.is_empty() {
396 keys.push(TrustedKey {
397 pubkey: old_pk,
398 expires_at_unix: Some(payload.rotated_at_unix + payload.grace_period_secs),
399 });
400 } else {
401 return Err(KeyRotationError::KeyRevoked);
402 }
403 }
404
405 for k in keys.iter_mut() {
407 if k.pubkey == old_pk {
408 k.expires_at_unix =
409 Some(payload.rotated_at_unix.saturating_add(payload.grace_period_secs));
410 }
411 }
412 if !keys.iter().any(|k| k.pubkey == new_pk) {
413 keys.push(TrustedKey {
414 pubkey: new_pk,
415 expires_at_unix: None,
416 });
417 }
418 }
419
420 self.applied
421 .lock()
422 .map_err(|_| KeyRotationError::LockPoisoned)?
423 .insert(fingerprint, now);
424
425 Ok(payload)
426 }
427
428 pub fn is_trusted(&self, node_id: &str, pubkey: &[u8; 32]) -> bool {
430 let now = self.clock.unix_time_us() / 1_000_000;
431 let Ok(map) = self.trusted.lock() else {
432 return false;
433 };
434 map.get(node_id)
435 .map(|keys| {
436 keys.iter().any(|k| {
437 k.pubkey == *pubkey
438 && match k.expires_at_unix {
439 Some(exp) => now < exp,
440 None => true,
441 }
442 })
443 })
444 .unwrap_or(false)
445 }
446
447 pub fn applied_count(&self) -> usize {
449 self.applied.lock().map(|m| m.len()).unwrap_or(0)
450 }
451}
452
453pub fn format_rotation_summary(
455 event: &ChainEvent,
456 payload: &KeyRotationPayload,
457 new_node_id: &str,
458) -> String {
459 let ts = DateTime::from_timestamp(payload.rotated_at_unix as i64, 0)
460 .unwrap_or_else(Utc::now);
461 format!(
462 "Key rotation recorded at chain sequence {}\n\
463 Old pubkey: {}…\n\
464 New pubkey: {}…\n\
465 New node id: {new_node_id}\n\
466 Grace period: {}s\n\
467 Rotated at: {}",
468 event.sequence,
469 &payload.old_pubkey_hex[..16.min(payload.old_pubkey_hex.len())],
470 &payload.new_pubkey_hex[..16.min(payload.new_pubkey_hex.len())],
471 payload.grace_period_secs,
472 ts.to_rfc3339(),
473 )
474}
475
476#[cfg(test)]
477mod tests {
478 use super::*;
479 use crate::mesh_clock::MockClock;
480 use std::time::Duration;
481
482 fn test_keys() -> (SigningKey, SigningKey) {
483 let old = SigningKey::from_bytes(&[1u8; 32]);
484 let new = SigningKey::from_bytes(&[2u8; 32]);
485 (old, new)
486 }
487
488 #[test]
489 fn dual_sign_happy_path() {
490 let (old, new) = test_keys();
491 let payload = build_key_rotation_payload(&old, &new, 3600, 1_700_000_000);
492 assert!(verify_key_rotation_payload(&payload).is_ok());
493 }
494
495 #[test]
496 fn rejects_missing_old_signature() {
497 let (old, new) = test_keys();
498 let mut payload = build_key_rotation_payload(&old, &new, 3600, 1_700_000_000);
499 payload.old_signature_hex = bytes_to_hex(&[0u8; 64]);
500 let err = verify_key_rotation_payload(&payload).unwrap_err();
501 assert!(err.to_string().contains("old-key"));
502 }
503
504 #[test]
505 fn rejects_missing_new_signature() {
506 let (old, new) = test_keys();
507 let mut payload = build_key_rotation_payload(&old, &new, 3600, 1_700_000_000);
508 let wrong = SigningKey::from_bytes(&[9u8; 32]);
510 let old_pk = old.verifying_key().to_bytes();
511 let new_pk = new.verifying_key().to_bytes();
512 let msg = KeyRotationPayload::canonical_message(&old_pk, &new_pk, 3600, 1_700_000_000);
513 payload.new_signature_hex = bytes_to_hex(&wrong.sign(&msg).to_bytes());
514 let err = verify_key_rotation_payload(&payload).unwrap_err();
515 assert!(err.to_string().contains("new-key"));
516 }
517
518 #[test]
519 fn rejects_only_one_of_two_signatures() {
520 let (old, new) = test_keys();
521 let mut payload = build_key_rotation_payload(&old, &new, 3600, 1_700_000_000);
522 payload.new_signature_hex = bytes_to_hex(&[0xAAu8; 64]);
524 assert!(verify_key_rotation_payload(&payload).is_err());
525 }
526
527 #[test]
528 fn chain_rotate_appends_and_swaps_key() {
529 let old = SigningKey::from_bytes(&[3u8; 32]);
530 let mut cm = ChainManager::new(0, 1000).with_signing_key(old.clone());
531 let old_vk = cm.verifying_key().unwrap();
532 let (event, new_key) = rotate_chain_signing_key_now(&mut cm, 7200).unwrap();
533 assert_eq!(event.kind, KIND_KEY_ROTATION);
534 assert_eq!(event.source, SOURCE_IDENTITY);
535 let payload = verify_key_rotation_event(&event).unwrap();
536 assert_eq!(
537 payload.old_pubkey_hex,
538 bytes_to_hex(&old_vk.to_bytes())
539 );
540 assert_eq!(
541 payload.new_pubkey_hex,
542 bytes_to_hex(&new_key.verifying_key().to_bytes())
543 );
544 assert_eq!(
546 cm.verifying_key().unwrap().to_bytes(),
547 new_key.verifying_key().to_bytes()
548 );
549 }
550
551 #[test]
552 fn verifier_accepts_then_rejects_replay() {
553 let clock = MockClock::new();
554 clock.set_unix_us(1_700_000_000_000_000);
555 let verifier = KeyRotationVerifier::new(clock.clone().shared());
556
557 let old = SigningKey::from_bytes(&[4u8; 32]);
558 let mut cm = ChainManager::new(0, 1000).with_signing_key(old);
559 let (event, _) = rotate_chain_signing_key(&mut cm, 3600, &clock).unwrap();
560
561 let node = "n-test";
562 verifier.apply_rotation(node, &event).unwrap();
563 assert_eq!(verifier.applied_count(), 1);
564
565 let err = verifier.apply_rotation(node, &event).unwrap_err();
567 assert!(matches!(err, KeyRotationError::Replay));
568 }
569
570 #[test]
571 fn grace_period_revokes_old_key() {
572 let clock = MockClock::new();
573 clock.set_unix_us(1_000_000_000_000_000); let verifier = KeyRotationVerifier::new(clock.clone().shared());
575
576 let old = SigningKey::from_bytes(&[5u8; 32]);
577 let old_pk = old.verifying_key().to_bytes();
578 verifier.trust_peer("peer-a", old_pk).unwrap();
579 assert!(verifier.is_trusted("peer-a", &old_pk));
580
581 let mut cm = ChainManager::new(0, 1000).with_signing_key(old);
582 let grace = 100u64;
583 let (event, new_key) = rotate_chain_signing_key(&mut cm, grace, &clock).unwrap();
584 let new_pk = new_key.verifying_key().to_bytes();
585 verifier.apply_rotation("peer-a", &event).unwrap();
586
587 assert!(verifier.is_trusted("peer-a", &old_pk));
589 assert!(verifier.is_trusted("peer-a", &new_pk));
590
591 clock.advance(Duration::from_secs(grace + 1));
593 assert!(!verifier.is_trusted("peer-a", &old_pk));
594 assert!(verifier.is_trusted("peer-a", &new_pk));
595 }
596
597 #[test]
598 fn rotate_without_key_errors() {
599 let mut cm = ChainManager::new(0, 1000);
600 let err = rotate_chain_signing_key_now(&mut cm, 60).unwrap_err();
601 assert!(matches!(err, KeyRotationError::NoSigningKey));
602 }
603}