Skip to main content

holographic_memory/core/provenance/
keri.rs

1// Copyright 2024-2026 WritersLogic Contributors
2// SPDX-License-Identifier: Apache-2.0
3
4use anyhow::{anyhow, Result};
5use ed25519_dalek::{Signer, SigningKey, Verifier, VerifyingKey};
6use serde::{Deserialize, Serialize};
7use sha2::{Digest, Sha256};
8
9use super::did;
10
11/// KERI event types per Key Event Receipt Infrastructure spec (IETF draft-ssmith-keri).
12#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "lowercase")]
14pub enum EventType {
15    Inception,
16    Rotation,
17    Interaction,
18}
19
20/// A KERI key event in the Key Event Log (KEL).
21#[derive(Clone, Debug, Serialize, Deserialize)]
22pub struct KeyEvent {
23    #[serde(rename = "v")]
24    pub version: String,
25    #[serde(rename = "t")]
26    pub event_type: EventType,
27    #[serde(rename = "d")]
28    pub digest: String,
29    #[serde(rename = "i")]
30    pub identifier: String,
31    #[serde(rename = "s")]
32    pub sequence: u64,
33    #[serde(rename = "kt")]
34    pub key_threshold: u64,
35    #[serde(rename = "k")]
36    pub keys: Vec<String>,
37    #[serde(rename = "nt", skip_serializing_if = "Option::is_none")]
38    pub next_threshold: Option<u64>,
39    #[serde(rename = "n", skip_serializing_if = "Option::is_none")]
40    pub next_keys_digest: Option<String>,
41    #[serde(rename = "p", skip_serializing_if = "Option::is_none")]
42    pub prior_digest: Option<String>,
43    #[serde(rename = "a", default, skip_serializing_if = "Vec::is_empty")]
44    pub anchors: Vec<serde_json::Value>,
45    pub signature: String,
46}
47
48/// A local Key Event Log for tracking key lifecycle.
49#[derive(Default)]
50pub struct KeyEventLog {
51    events: Vec<KeyEvent>,
52    path: Option<std::path::PathBuf>,
53}
54
55impl KeyEventLog {
56    pub fn new() -> Self {
57        Self {
58            events: Vec::new(),
59            path: None,
60        }
61    }
62
63    pub fn with_path(path: &std::path::Path) -> Result<Self> {
64        let events = if path.exists() {
65            let data =
66                std::fs::read_to_string(path).map_err(|e| anyhow!("failed to read KEL: {e}"))?;
67            serde_json::from_str(&data).map_err(|e| anyhow!("failed to parse KEL: {e}"))?
68        } else {
69            Vec::new()
70        };
71        Ok(Self {
72            events,
73            path: Some(path.to_path_buf()),
74        })
75    }
76
77    fn persist(&self) -> Result<()> {
78        if let Some(ref path) = self.path {
79            let data = serde_json::to_vec_pretty(&self.events)
80                .map_err(|e| anyhow!("KEL serialization failed: {e}"))?;
81            std::fs::write(path, data).map_err(|e| anyhow!("KEL write failed: {e}"))?;
82        }
83        Ok(())
84    }
85
86    pub fn events(&self) -> &[KeyEvent] {
87        &self.events
88    }
89
90    pub fn current_keys(&self) -> Option<&[String]> {
91        self.events.last().map(|e| e.keys.as_slice())
92    }
93
94    pub fn latest_event(&self) -> Option<&KeyEvent> {
95        self.events.last()
96    }
97
98    /// Create an inception event establishing the identifier with an initial key.
99    pub fn inception(
100        &mut self,
101        signing_key: &SigningKey,
102        next_key_digest: Option<&str>,
103    ) -> Result<&KeyEvent> {
104        if !self.events.is_empty() {
105            return Err(anyhow!("KEL already has an inception event"));
106        }
107
108        let pk_bytes = signing_key.verifying_key().to_bytes();
109        let identifier = did::did_key_from_ed25519(&pk_bytes);
110        let current_key = did::did_key_from_ed25519(&pk_bytes);
111
112        let mut event = KeyEvent {
113            version: "KERI10JSON".to_string(),
114            event_type: EventType::Inception,
115            digest: String::new(),
116            identifier: identifier.clone(),
117            sequence: 0,
118            key_threshold: 1,
119            keys: vec![current_key],
120            next_threshold: next_key_digest.map(|_| 1),
121            next_keys_digest: next_key_digest.map(String::from),
122            prior_digest: None,
123            anchors: Vec::new(),
124            signature: String::new(),
125        };
126
127        event.digest = self_addressing_digest(&event)?;
128        event.signature = sign_event(signing_key, &event)?;
129
130        self.events.push(event);
131        self.persist()?;
132        Ok(self.events.last().unwrap())
133    }
134
135    /// Create a rotation event replacing the current key with a new key.
136    pub fn rotation(
137        &mut self,
138        current_key: &SigningKey,
139        new_key: &SigningKey,
140        next_key_digest: Option<&str>,
141    ) -> Result<&KeyEvent> {
142        let prior = self
143            .events
144            .last()
145            .ok_or_else(|| anyhow!("no prior event for rotation"))?;
146
147        let new_pk = new_key.verifying_key().to_bytes();
148        let new_did = did::did_key_from_ed25519(&new_pk);
149
150        let mut event = KeyEvent {
151            version: "KERI10JSON".to_string(),
152            event_type: EventType::Rotation,
153            digest: String::new(),
154            identifier: prior.identifier.clone(),
155            sequence: prior.sequence + 1,
156            key_threshold: 1,
157            keys: vec![new_did],
158            next_threshold: next_key_digest.map(|_| 1),
159            next_keys_digest: next_key_digest.map(String::from),
160            prior_digest: Some(prior.digest.clone()),
161            anchors: Vec::new(),
162            signature: String::new(),
163        };
164
165        event.digest = self_addressing_digest(&event)?;
166        event.signature = sign_event(current_key, &event)?;
167
168        self.events.push(event);
169        self.persist()?;
170        Ok(self.events.last().unwrap())
171    }
172
173    /// Create an interaction event anchoring data (seals) without rotating keys.
174    pub fn interaction(
175        &mut self,
176        signing_key: &SigningKey,
177        anchors: Vec<serde_json::Value>,
178    ) -> Result<&KeyEvent> {
179        let prior = self
180            .events
181            .last()
182            .ok_or_else(|| anyhow!("no prior event for interaction"))?;
183
184        let mut event = KeyEvent {
185            version: "KERI10JSON".to_string(),
186            event_type: EventType::Interaction,
187            digest: String::new(),
188            identifier: prior.identifier.clone(),
189            sequence: prior.sequence + 1,
190            key_threshold: prior.key_threshold,
191            keys: prior.keys.clone(),
192            next_threshold: prior.next_threshold,
193            next_keys_digest: prior.next_keys_digest.clone(),
194            prior_digest: Some(prior.digest.clone()),
195            anchors,
196            signature: String::new(),
197        };
198
199        event.digest = self_addressing_digest(&event)?;
200        event.signature = sign_event(signing_key, &event)?;
201
202        self.events.push(event);
203        self.persist()?;
204        Ok(self.events.last().unwrap())
205    }
206
207    /// Verify the entire KEL chain: each event's digest and signature, plus chain linkage.
208    pub fn verify(&self) -> Result<()> {
209        if self.events.is_empty() {
210            return Err(anyhow!("empty KEL"));
211        }
212
213        if self.events[0].event_type != EventType::Inception {
214            return Err(anyhow!("first event must be inception"));
215        }
216
217        let mut current_keys = Vec::new();
218
219        for (i, event) in self.events.iter().enumerate() {
220            if event.sequence != i as u64 {
221                return Err(anyhow!(
222                    "sequence gap: expected {i}, got {}",
223                    event.sequence
224                ));
225            }
226
227            // A KeyEvent carries a single signature, so it cannot represent an
228            // m-of-n multisig. Reject kt>1 rather than silently accepting one
229            // signature as satisfying a multisig threshold.
230            if event.key_threshold > 1 {
231                return Err(anyhow!(
232                    "multisig key threshold ({}) not supported at event {i}",
233                    event.key_threshold
234                ));
235            }
236
237            let computed = self_addressing_digest(event)?;
238            if computed != event.digest {
239                return Err(anyhow!("digest mismatch at event {i}"));
240            }
241
242            if i > 0 {
243                let prior = &self.events[i - 1];
244                if event
245                    .prior_digest
246                    .as_ref()
247                    .is_none_or(|d| d != &prior.digest)
248                {
249                    return Err(anyhow!("chain break at event {i}"));
250                }
251
252                // Pre-rotation: a rotation may only install keys the prior event
253                // committed to via its next-key digest. This is KERI's core
254                // security property — a compromised current key cannot rotate to
255                // an attacker-chosen key.
256                if event.event_type == EventType::Rotation {
257                    let committed = prior.next_keys_digest.as_ref().ok_or_else(|| {
258                        anyhow!("rotation at event {i} but prior event pre-committed no next key")
259                    })?;
260                    if &next_keys_digest(&event.keys)? != committed {
261                        return Err(anyhow!(
262                            "rotation at event {i} installs keys not matching the prior commitment"
263                        ));
264                    }
265                }
266            }
267
268            if i == 0 {
269                verify_event_signature(event, &event.keys)?;
270            } else {
271                verify_event_signature(event, &current_keys)?;
272            }
273
274            current_keys.clone_from(&event.keys);
275        }
276
277        Ok(())
278    }
279}
280
281/// Canonical digest committing to a next authorized key set: SHA-256 over the
282/// concatenated raw Ed25519 public keys decoded from each `did:key`, hex-encoded.
283/// For a single key this equals SHA-256(pk_bytes), matching how callers derive
284/// the pre-rotation commitment passed to `inception`/`rotation`.
285fn next_keys_digest(keys: &[String]) -> Result<String> {
286    let mut hasher = Sha256::new();
287    for key_did in keys {
288        let pk = did::ed25519_from_did_key(key_did)?;
289        hasher.update(pk);
290    }
291    let hash = hasher.finalize();
292    Ok(hash.iter().map(|b| format!("{b:02x}")).collect())
293}
294
295fn self_addressing_digest(event: &KeyEvent) -> Result<String> {
296    let mut e = event.clone();
297    e.digest = String::new();
298    e.signature = String::new();
299    let serialized =
300        serde_json::to_vec(&e).map_err(|e| anyhow!("event serialization failed: {e}"))?;
301    let hash = Sha256::digest(&serialized);
302    Ok(hash.iter().map(|b| format!("{b:02x}")).collect())
303}
304
305fn sign_event(signing_key: &SigningKey, event: &KeyEvent) -> Result<String> {
306    let mut e = event.clone();
307    e.signature = String::new();
308    let serialized =
309        serde_json::to_vec(&e).map_err(|e| anyhow!("event serialization failed: {e}"))?;
310    let signature = signing_key.sign(&serialized);
311    Ok(multibase::encode(
312        multibase::Base::Base58Btc,
313        signature.to_bytes(),
314    ))
315}
316
317fn verify_event_signature(event: &KeyEvent, signer_keys: &[String]) -> Result<()> {
318    let mut e = event.clone();
319    e.signature = String::new();
320    let serialized =
321        serde_json::to_vec(&e).map_err(|e| anyhow!("event serialization failed: {e}"))?;
322
323    let (_, sig_bytes) =
324        multibase::decode(&event.signature).map_err(|e| anyhow!("multibase decode failed: {e}"))?;
325    let signature = ed25519_dalek::Signature::from_slice(&sig_bytes)
326        .map_err(|e| anyhow!("invalid signature: {e}"))?;
327
328    for key_did in signer_keys {
329        if let Ok(pk_bytes) = did::ed25519_from_did_key(key_did) {
330            if let Ok(vk) = VerifyingKey::from_bytes(&pk_bytes) {
331                if vk.verify(&serialized, &signature).is_ok() {
332                    return Ok(());
333                }
334            }
335        }
336    }
337
338    Err(anyhow!("no authorized key verified the signature"))
339}
340
341#[cfg(test)]
342mod tests {
343    use super::*;
344
345    fn test_keypair() -> SigningKey {
346        SigningKey::from_bytes(&rand::random())
347    }
348
349    fn next_key_digest(key: &SigningKey) -> String {
350        let pk = key.verifying_key().to_bytes();
351        let hash = Sha256::digest(pk);
352        hash.iter().map(|b| format!("{b:02x}")).collect()
353    }
354
355    #[test]
356    fn inception_and_verify() {
357        let key = test_keypair();
358        let mut kel = KeyEventLog::new();
359        let event = kel.inception(&key, None).unwrap();
360
361        assert_eq!(event.event_type, EventType::Inception);
362        assert_eq!(event.sequence, 0);
363        assert!(!event.digest.is_empty());
364        kel.verify().unwrap();
365    }
366
367    #[test]
368    fn rotation_and_verify() {
369        let key1 = test_keypair();
370        let key2 = test_keypair();
371        let mut kel = KeyEventLog::new();
372
373        let nkd = next_key_digest(&key2);
374        kel.inception(&key1, Some(&nkd)).unwrap();
375        let rot = kel.rotation(&key1, &key2, None).unwrap();
376
377        assert_eq!(rot.event_type, EventType::Rotation);
378        assert_eq!(rot.sequence, 1);
379        assert!(rot.prior_digest.is_some());
380        kel.verify().unwrap();
381    }
382
383    #[test]
384    fn interaction_anchors_data() {
385        let key = test_keypair();
386        let mut kel = KeyEventLog::new();
387        kel.inception(&key, None).unwrap();
388
389        let seal = serde_json::json!({"i": "fact-123", "d": "abc123"});
390        let ixn = kel.interaction(&key, vec![seal.clone()]).unwrap();
391
392        assert_eq!(ixn.event_type, EventType::Interaction);
393        assert_eq!(ixn.anchors.len(), 1);
394        kel.verify().unwrap();
395    }
396
397    #[test]
398    fn tampered_event_rejected() {
399        let key = test_keypair();
400        let mut kel = KeyEventLog::new();
401        kel.inception(&key, None).unwrap();
402        kel.interaction(&key, Vec::new()).unwrap();
403
404        kel.events[1].anchors = vec![serde_json::json!({"tampered": true})];
405        assert!(kel.verify().is_err());
406    }
407
408    #[test]
409    fn rotation_to_uncommitted_key_rejected() {
410        // Inception pre-commits to key2, but the holder rotates to key3 instead.
411        // Pre-rotation enforcement must reject the chain even though the
412        // rotation event is validly signed by the current key.
413        let key1 = test_keypair();
414        let key2 = test_keypair();
415        let key3 = test_keypair();
416        let mut kel = KeyEventLog::new();
417
418        kel.inception(&key1, Some(&next_key_digest(&key2))).unwrap();
419        kel.rotation(&key1, &key3, None).unwrap();
420
421        assert!(kel.verify().is_err());
422    }
423
424    #[test]
425    fn rotation_without_precommitment_rejected() {
426        // Inception commits no next key, so any rotation is unauthorized.
427        let key1 = test_keypair();
428        let key2 = test_keypair();
429        let mut kel = KeyEventLog::new();
430
431        kel.inception(&key1, None).unwrap();
432        kel.rotation(&key1, &key2, None).unwrap();
433
434        assert!(kel.verify().is_err());
435    }
436
437    #[test]
438    fn multisig_threshold_rejected() {
439        let key = test_keypair();
440        let mut kel = KeyEventLog::new();
441        kel.inception(&key, None).unwrap();
442        kel.events[0].key_threshold = 2;
443
444        assert!(kel.verify().is_err());
445    }
446
447    #[test]
448    fn double_inception_rejected() {
449        let key = test_keypair();
450        let mut kel = KeyEventLog::new();
451        kel.inception(&key, None).unwrap();
452        assert!(kel.inception(&key, None).is_err());
453    }
454
455    #[test]
456    fn full_lifecycle() {
457        let key1 = test_keypair();
458        let key2 = test_keypair();
459        let key3 = test_keypair();
460        let mut kel = KeyEventLog::new();
461
462        kel.inception(&key1, Some(&next_key_digest(&key2))).unwrap();
463        kel.interaction(&key1, vec![serde_json::json!({"seal": "a"})])
464            .unwrap();
465        kel.rotation(&key1, &key2, Some(&next_key_digest(&key3)))
466            .unwrap();
467        kel.interaction(&key2, vec![serde_json::json!({"seal": "b"})])
468            .unwrap();
469        kel.rotation(&key2, &key3, None).unwrap();
470
471        assert_eq!(kel.events().len(), 5);
472        kel.verify().unwrap();
473
474        let current = kel.current_keys().unwrap();
475        let key3_did = did::did_key_from_ed25519(&key3.verifying_key().to_bytes());
476        assert_eq!(current, &[key3_did]);
477    }
478
479    #[test]
480    fn persistence_roundtrip() {
481        let dir = tempfile::tempdir().unwrap();
482        let kel_path = dir.path().join("kel.json");
483
484        let key1 = test_keypair();
485        let key2 = test_keypair();
486
487        {
488            let mut kel = KeyEventLog::with_path(&kel_path).unwrap();
489            kel.inception(&key1, Some(&next_key_digest(&key2))).unwrap();
490            kel.interaction(&key1, vec![serde_json::json!({"fact": "f1"})])
491                .unwrap();
492            kel.rotation(&key1, &key2, None).unwrap();
493            assert_eq!(kel.events().len(), 3);
494        }
495
496        let loaded = KeyEventLog::with_path(&kel_path).unwrap();
497        assert_eq!(loaded.events().len(), 3);
498        loaded.verify().unwrap();
499        let key2_did = did::did_key_from_ed25519(&key2.verifying_key().to_bytes());
500        assert_eq!(loaded.current_keys().unwrap(), &[key2_did]);
501    }
502}