Skip to main content

clawft_kernel/
key_rotation.rs

1//! S10 key rotation: dual-signed ExoChain event + verifier (WEFT-107).
2//!
3//! Security-panel protocol ([`docs/weftos/k5-symposium/03-security-and-identity.md`]):
4//!
5//! 1. Node generates a new Ed25519 keypair.
6//! 2. Node signs a `identity.key_rotation` chain event with the **old** key,
7//!    embedding the new public key.
8//! 3. Node signs the same canonical message with the **new** key (possession).
9//! 4. Peers verify both signatures and update peer records.
10//! 5. Old key is revoked after a configurable grace period.
11//!
12//! The dual signature lives in the chain event **payload**. Event hash
13//! linking remains SHAKE-256 as for every other ExoChain entry.
14
15use 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
27/// Chain event kind for S10 key rotation.
28pub const KIND_KEY_ROTATION: &str = "identity.key_rotation";
29
30/// Chain event source for identity / key lifecycle events.
31pub const SOURCE_IDENTITY: &str = "identity";
32
33/// Default grace period before the old key is fully revoked (24 hours).
34pub const DEFAULT_GRACE_PERIOD_SECS: u64 = 86_400;
35
36/// Domain-separated prefix for the dual-signed canonical message.
37const CANONICAL_PREFIX: &str = "weftos-key-rotation:v1";
38
39/// Errors from building or verifying a key-rotation event.
40#[derive(Debug, thiserror::Error)]
41pub enum KeyRotationError {
42    /// Chain manager has no Ed25519 signing key configured.
43    #[error("no signing key configured on chain manager")]
44    NoSigningKey,
45    /// Payload failed dual-signature verification.
46    #[error("key rotation verification failed: {0}")]
47    Verification(String),
48    /// Signature bytes are not a valid Ed25519 signature encoding.
49    #[error("invalid signature encoding: {0}")]
50    BadSignature(String),
51    /// Public key bytes are not a valid Ed25519 verifying key.
52    #[error("invalid public key encoding: {0}")]
53    BadPublicKey(String),
54    /// Replay of an already-accepted rotation for this (old, new) pair.
55    #[error("key rotation replay rejected: already applied for this key pair")]
56    Replay,
57    /// Old key is past its grace period and cannot sign further rotations.
58    #[error("old key is past grace period and has been revoked")]
59    KeyRevoked,
60    /// Internal lock poison.
61    #[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/// Dual-signed key-rotation payload embedded in a chain event.
85#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
86pub struct KeyRotationPayload {
87    /// Previous Ed25519 public key (32 bytes, hex-encoded).
88    pub old_pubkey_hex: String,
89    /// New Ed25519 public key (32 bytes, hex-encoded).
90    pub new_pubkey_hex: String,
91    /// Ed25519 signature over the canonical message by the old key (64 bytes, hex).
92    pub old_signature_hex: String,
93    /// Ed25519 signature over the canonical message by the new key (64 bytes, hex).
94    pub new_signature_hex: String,
95    /// Seconds after `rotated_at_unix` during which the old key remains
96    /// accepted for verification (grace / overlap window).
97    pub grace_period_secs: u64,
98    /// Unix timestamp (seconds) when the rotation was produced.
99    pub rotated_at_unix: u64,
100}
101
102impl KeyRotationPayload {
103    /// Build the domain-separated canonical bytes both keys must sign.
104    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    /// Decode hex pubkey to 32 raw bytes.
123    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    /// Decode hex signature to 64 raw bytes.
137    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    /// Parse payload from a chain event JSON value.
154    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
163/// Build a dual-signed key-rotation payload.
164///
165/// Signs the canonical message with both `old_key` and `new_key`.
166pub 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
192/// Verify both Ed25519 signatures on a key-rotation payload.
193///
194/// Rejects if either signature fails, keys are malformed, or the
195/// embedded pubkeys do not match the signers of their signatures.
196pub 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
235/// Verify a chain event that claims to be a key rotation.
236pub 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
252/// Perform a local key rotation on a [`ChainManager`].
253///
254/// Generates a new Ed25519 keypair, dual-signs a rotation payload,
255/// appends `identity.key_rotation` to the chain, and swaps the
256/// manager's signing key to the new key.
257///
258/// Returns `(event, new_signing_key)`.
259pub 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    // Defense in depth: never append an unverifiable payload.
272    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
289/// Convenience: rotate using [`RealClock`].
290pub 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
297/// Tracks accepted key rotations for peer verification + grace windows.
298///
299/// During the grace period both the old and new pubkeys of a node are
300/// trusted for signature verification. After expiry the old key is
301/// revoked. Replays of the same (old, new) pair are rejected.
302pub struct KeyRotationVerifier {
303    /// node_id → currently trusted verifying keys (with optional expiry).
304    trusted: Mutex<HashMap<String, Vec<TrustedKey>>>,
305    /// Fingerprints of applied rotations (old||new hex) for replay defense.
306    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    /// Unix seconds after which this key is no longer trusted.
314    /// `None` = no expiry (current primary key).
315    expires_at_unix: Option<u64>,
316}
317
318impl KeyRotationVerifier {
319    /// Create a verifier bound to the given clock (production: RealClock).
320    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    /// Production default with [`RealClock`].
329    pub fn with_real_clock() -> Self {
330        Self::new(RealClock::shared())
331    }
332
333    /// Seed a peer's current public key (no expiry).
334    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    /// Apply a verified key-rotation event for `node_id`.
350    ///
351    /// Rejects:
352    /// - Invalid dual signatures
353    /// - Replay of the same (old, new) pair
354    /// - Rotations where the claimed old key is not currently trusted
355    /// - Rotations where the old key is past its grace period
356    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            // Prune expired keys and require old_pk currently trusted.
388            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                // First rotation for this peer may seed from event if empty.
395                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            // Mark old key with grace expiry; add new key as primary.
406            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    /// Whether `pubkey` is currently trusted for `node_id` (grace-aware).
429    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    /// Number of applied (non-replay) rotations.
448    pub fn applied_count(&self) -> usize {
449        self.applied.lock().map(|m| m.len()).unwrap_or(0)
450    }
451}
452
453/// Format a human-readable rotation summary for CLI output.
454pub 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        // Sign with wrong key for new slot.
509        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        // Valid-length garbage sig.
523        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        // Manager now holds the new key.
545        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        // Replay of same event must fail.
566        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); // 1e9 seconds
574        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        // During grace both keys trusted.
588        assert!(verifier.is_trusted("peer-a", &old_pk));
589        assert!(verifier.is_trusted("peer-a", &new_pk));
590
591        // Advance past grace.
592        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}