Skip to main content

auths_id/keri/
state.rs

1//! Key state derived from replaying a KERI event log.
2//!
3//! The `KeyState` represents the current cryptographic state of a KERI
4//! identity after processing all events in its KEL. This is the "resolved"
5//! state used for signature verification and capability checking.
6
7use serde::{Deserialize, Serialize};
8
9use super::types::{Prefix, Said};
10
11/// Current key state derived from replaying a KEL.
12///
13/// This struct captures the complete state of a KERI identity at a given
14/// point in its event log. It is computed by walking the KEL from inception
15/// to the latest event.
16#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
17pub struct KeyState {
18    /// The KERI identifier prefix (used in `did:keri:<prefix>`)
19    pub prefix: Prefix,
20
21    /// Current signing key(s), Base64url encoded with derivation code prefix.
22    /// For Ed25519 keys, this is "D" + base64url(pubkey).
23    pub current_keys: Vec<String>,
24
25    /// Next key commitment(s) for pre-rotation.
26    /// These are Blake3 hashes of the next public key(s).
27    pub next_commitment: Vec<String>,
28
29    /// Current sequence number (0 for inception, increments with each event)
30    pub sequence: u64,
31
32    /// SAID of the last processed event
33    pub last_event_said: Said,
34
35    /// Whether this identity has been abandoned (empty next commitment)
36    pub is_abandoned: bool,
37    /// Current signing threshold
38    pub threshold: u64,
39    /// Next signing threshold (committed)
40    pub next_threshold: u64,
41}
42
43impl KeyState {
44    /// Create initial state from an inception event.
45    ///
46    /// # Arguments
47    /// * `prefix` - The KERI identifier (same as inception SAID)
48    /// * `keys` - The initial signing key(s)
49    /// * `next` - The next-key commitment(s)
50    /// * `threshold` - Initial signing threshold
51    /// * `next_threshold` - Committed next signing threshold
52    /// * `said` - The inception event SAID
53    pub fn from_inception(
54        prefix: Prefix,
55        keys: Vec<String>,
56        next: Vec<String>,
57        threshold: u64,
58        next_threshold: u64,
59        said: Said,
60    ) -> Self {
61        Self {
62            prefix,
63            current_keys: keys,
64            next_commitment: next.clone(),
65            sequence: 0,
66            last_event_said: said,
67            is_abandoned: next.is_empty(),
68            threshold,
69            next_threshold,
70        }
71    }
72
73    /// Apply a rotation event to update state.
74    ///
75    /// This should only be called after verifying:
76    /// 1. The new key matches the previous next_commitment
77    /// 2. The event's previous SAID matches last_event_said
78    /// 3. The sequence is exactly last_sequence + 1
79    pub fn apply_rotation(
80        &mut self,
81        new_keys: Vec<String>,
82        new_next: Vec<String>,
83        threshold: u64,
84        next_threshold: u64,
85        sequence: u64,
86        said: Said,
87    ) {
88        self.current_keys = new_keys;
89        self.next_commitment = new_next.clone();
90        self.threshold = threshold;
91        self.next_threshold = next_threshold;
92        self.sequence = sequence;
93        self.last_event_said = said;
94        self.is_abandoned = new_next.is_empty();
95    }
96
97    /// Apply an interaction event (updates sequence and SAID only).
98    ///
99    /// Interaction events anchor data but don't change keys.
100    pub fn apply_interaction(&mut self, sequence: u64, said: Said) {
101        self.sequence = sequence;
102        self.last_event_said = said;
103    }
104
105    /// Get the current signing key (first key for single-sig).
106    ///
107    /// Returns the encoded key string (e.g., "DBase64EncodedKey...")
108    pub fn current_key(&self) -> Option<&str> {
109        self.current_keys.first().map(|s| s.as_str())
110    }
111
112    /// Check if key can be rotated.
113    ///
114    /// Returns `false` if the identity has been abandoned (empty next commitment).
115    pub fn can_rotate(&self) -> bool {
116        !self.is_abandoned && !self.next_commitment.is_empty()
117    }
118
119    /// Get the DID for this identity.
120    pub fn did(&self) -> String {
121        format!("did:keri:{}", self.prefix.as_str())
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn key_state_from_inception() {
131        let state = KeyState::from_inception(
132            Prefix::new_unchecked("EPrefix".to_string()),
133            vec!["DKey1".to_string()],
134            vec!["ENext1".to_string()],
135            1,
136            1,
137            Said::new_unchecked("ESAID".to_string()),
138        );
139        assert_eq!(state.sequence, 0);
140        assert!(!state.is_abandoned);
141        assert!(state.can_rotate());
142        assert_eq!(state.current_key(), Some("DKey1"));
143        assert_eq!(state.did(), "did:keri:EPrefix");
144    }
145
146    #[test]
147    fn key_state_apply_rotation() {
148        let mut state = KeyState::from_inception(
149            Prefix::new_unchecked("EPrefix".to_string()),
150            vec!["DKey1".to_string()],
151            vec!["ENext1".to_string()],
152            1,
153            1,
154            Said::new_unchecked("ESAID1".to_string()),
155        );
156
157        state.apply_rotation(
158            vec!["DKey2".to_string()],
159            vec!["ENext2".to_string()],
160            1,
161            1,
162            1,
163            Said::new_unchecked("ESAID2".to_string()),
164        );
165
166        assert_eq!(state.sequence, 1);
167        assert_eq!(state.current_keys[0], "DKey2");
168        assert_eq!(state.next_commitment[0], "ENext2");
169        assert_eq!(state.last_event_said, "ESAID2");
170        assert!(state.can_rotate());
171    }
172
173    #[test]
174    fn key_state_apply_interaction() {
175        let mut state = KeyState::from_inception(
176            Prefix::new_unchecked("EPrefix".to_string()),
177            vec!["DKey1".to_string()],
178            vec!["ENext1".to_string()],
179            1,
180            1,
181            Said::new_unchecked("ESAID1".to_string()),
182        );
183
184        state.apply_interaction(1, Said::new_unchecked("ESAID_IXN".to_string()));
185
186        assert_eq!(state.sequence, 1);
187        // Keys should not change
188        assert_eq!(state.current_keys[0], "DKey1");
189        assert_eq!(state.last_event_said, "ESAID_IXN");
190    }
191
192    #[test]
193    fn abandoned_identity_cannot_rotate() {
194        let state = KeyState::from_inception(
195            Prefix::new_unchecked("EPrefix".to_string()),
196            vec!["DKey1".to_string()],
197            vec![], // Empty next commitment = abandoned
198            1,
199            0,
200            Said::new_unchecked("ESAID".to_string()),
201        );
202        assert!(state.is_abandoned);
203        assert!(!state.can_rotate());
204    }
205
206    #[test]
207    fn key_state_serializes() {
208        let state = KeyState::from_inception(
209            Prefix::new_unchecked("EPrefix".to_string()),
210            vec!["DKey1".to_string()],
211            vec!["ENext1".to_string()],
212            1,
213            1,
214            Said::new_unchecked("ESAID".to_string()),
215        );
216
217        let json = serde_json::to_string(&state).unwrap();
218        let parsed: KeyState = serde_json::from_str(&json).unwrap();
219        assert_eq!(state, parsed);
220    }
221}