1use std::ops::ControlFlow;
9
10use auths_crypto::Pkcs8Der;
11use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
12use git2::Repository;
13use ring::rand::SystemRandom;
14use ring::signature::{Ed25519KeyPair, KeyPair};
15
16use auths_core::crypto::said::{compute_next_commitment, compute_said, verify_commitment};
17
18use super::event::KeriSequence;
19use super::types::{Prefix, Said};
20use super::{
21 Event, GitKel, KERI_VERSION, KelError, KeyState, RotEvent, ValidationError, validate_kel,
22};
23use crate::storage::registry::backend::{RegistryBackend, RegistryError};
24use crate::witness_config::WitnessConfig;
25
26#[derive(Debug, thiserror::Error)]
28#[non_exhaustive]
29pub enum RotationError {
30 #[error("Key generation failed: {0}")]
31 KeyGeneration(String),
32
33 #[error("KEL error: {0}")]
34 Kel(#[from] KelError),
35
36 #[error("Storage error: {0}")]
37 Storage(RegistryError),
38
39 #[error("Validation error: {0}")]
40 Validation(#[from] ValidationError),
41
42 #[error("Identity is abandoned (cannot rotate)")]
43 IdentityAbandoned,
44
45 #[error("Commitment mismatch: next key does not match previous commitment")]
46 CommitmentMismatch,
47
48 #[error("Serialization error: {0}")]
49 Serialization(String),
50
51 #[error("Invalid key: {0}")]
52 InvalidKey(String),
53}
54
55impl auths_core::error::AuthsErrorInfo for RotationError {
56 fn error_code(&self) -> &'static str {
57 match self {
58 Self::KeyGeneration(_) => "AUTHS-E4701",
59 Self::Kel(_) => "AUTHS-E4702",
60 Self::Storage(_) => "AUTHS-E4703",
61 Self::Validation(_) => "AUTHS-E4704",
62 Self::IdentityAbandoned => "AUTHS-E4705",
63 Self::CommitmentMismatch => "AUTHS-E4706",
64 Self::Serialization(_) => "AUTHS-E4707",
65 Self::InvalidKey(_) => "AUTHS-E4708",
66 }
67 }
68
69 fn suggestion(&self) -> Option<&'static str> {
70 match self {
71 Self::KeyGeneration(_) => None,
72 Self::Kel(_) => Some("Check the KEL state for the identity"),
73 Self::Storage(_) => Some("Check storage backend connectivity"),
74 Self::Validation(_) => None,
75 Self::IdentityAbandoned => {
76 Some("This identity has been abandoned and cannot be rotated")
77 }
78 Self::CommitmentMismatch => {
79 Some("The provided next key does not match the pre-rotation commitment")
80 }
81 Self::Serialization(_) => None,
82 Self::InvalidKey(_) => Some("Provide a valid Ed25519 key in PKCS#8 format"),
83 }
84 }
85}
86
87pub struct RotationResult {
89 pub prefix: Prefix,
91
92 pub sequence: u64,
94
95 pub new_current_keypair_pkcs8: Pkcs8Der,
97
98 pub new_next_keypair_pkcs8: Pkcs8Der,
100
101 pub new_current_public_key: Vec<u8>,
103
104 pub new_next_public_key: Vec<u8>,
106}
107
108impl std::fmt::Debug for RotationResult {
109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 f.debug_struct("RotationResult")
111 .field("prefix", &self.prefix)
112 .field("sequence", &self.sequence)
113 .field("new_current_keypair_pkcs8", &"[REDACTED]")
114 .field("new_next_keypair_pkcs8", &"[REDACTED]")
115 .field("new_current_public_key", &self.new_current_public_key)
116 .field("new_next_public_key", &self.new_next_public_key)
117 .finish()
118 }
119}
120
121pub fn rotate_keys(
136 repo: &Repository,
137 prefix: &Prefix,
138 next_keypair_pkcs8: &Pkcs8Der,
139 witness_config: Option<&WitnessConfig>,
140 now: chrono::DateTime<chrono::Utc>,
141) -> Result<RotationResult, RotationError> {
142 let rng = SystemRandom::new();
143
144 let kel = GitKel::new(repo, prefix.as_str());
146 let events = kel.get_events()?;
147 let state = validate_kel(&events)?;
148
149 if !state.can_rotate() {
151 return Err(RotationError::IdentityAbandoned);
152 }
153
154 let next_keypair =
156 crate::identity::helpers::load_keypair_from_der_or_seed(next_keypair_pkcs8.as_ref())
157 .map_err(|e| RotationError::InvalidKey(e.to_string()))?;
158
159 if !verify_commitment(
161 next_keypair.public_key().as_ref(),
162 &state.next_commitment[0],
163 ) {
164 return Err(RotationError::CommitmentMismatch);
165 }
166
167 let new_next_pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng)
169 .map_err(|e| RotationError::KeyGeneration(e.to_string()))?;
170 let new_next_keypair = Ed25519KeyPair::from_pkcs8(new_next_pkcs8.as_ref())
171 .map_err(|e| RotationError::KeyGeneration(e.to_string()))?;
172
173 let new_current_pub_encoded = format!(
175 "D{}",
176 URL_SAFE_NO_PAD.encode(next_keypair.public_key().as_ref())
177 );
178
179 let new_next_commitment = compute_next_commitment(new_next_keypair.public_key().as_ref());
181
182 let (bt, b) = match witness_config {
184 Some(cfg) if cfg.is_enabled() => (
185 cfg.threshold.to_string(),
186 cfg.witness_urls.iter().map(|u| u.to_string()).collect(),
187 ),
188 _ => ("0".to_string(), vec![]),
189 };
190
191 let new_sequence = state.sequence + 1;
193 let mut rot = RotEvent {
194 v: KERI_VERSION.to_string(),
195 d: Said::default(),
196 i: prefix.clone(),
197 s: KeriSequence::new(new_sequence),
198 p: state.last_event_said.clone(),
199 kt: "1".to_string(),
200 k: vec![new_current_pub_encoded],
201 nt: "1".to_string(),
202 n: vec![new_next_commitment],
203 bt,
204 b,
205 a: vec![],
206 x: String::new(),
207 };
208
209 let rot_json = serde_json::to_vec(&Event::Rot(rot.clone()))
211 .map_err(|e| RotationError::Serialization(e.to_string()))?;
212 rot.d = compute_said(&rot_json);
213
214 let canonical = super::serialize_for_signing(&Event::Rot(rot.clone()))?;
216 let sig = next_keypair.sign(&canonical);
217 rot.x = URL_SAFE_NO_PAD.encode(sig.as_ref());
218
219 kel.append(&Event::Rot(rot.clone()), now)?;
221
222 #[cfg(feature = "witness-client")]
224 if let Some(config) = witness_config
225 && config.is_enabled()
226 {
227 let canonical_for_witness = super::serialize_for_signing(&Event::Rot(rot.clone()))?;
228 super::witness_integration::collect_and_store_receipts(
229 repo.path().parent().unwrap_or(repo.path()),
230 prefix,
231 &rot.d,
232 &canonical_for_witness,
233 config,
234 now,
235 )
236 .map_err(|e| RotationError::Serialization(e.to_string()))?;
237 }
238
239 Ok(RotationResult {
240 prefix: prefix.clone(),
241 sequence: new_sequence,
242 new_current_keypair_pkcs8: next_keypair_pkcs8.clone(),
243 new_next_keypair_pkcs8: Pkcs8Der::new(new_next_pkcs8.as_ref()),
244 new_current_public_key: next_keypair.public_key().as_ref().to_vec(),
245 new_next_public_key: new_next_keypair.public_key().as_ref().to_vec(),
246 })
247}
248
249pub fn abandon_identity(
260 repo: &Repository,
261 prefix: &Prefix,
262 next_keypair_pkcs8: &Pkcs8Der,
263 witness_config: Option<&WitnessConfig>,
264 now: chrono::DateTime<chrono::Utc>,
265) -> Result<u64, RotationError> {
266 let kel = GitKel::new(repo, prefix.as_str());
268 let events = kel.get_events()?;
269 let state = validate_kel(&events)?;
270
271 if state.is_abandoned {
273 return Err(RotationError::IdentityAbandoned);
274 }
275
276 let next_keypair = Ed25519KeyPair::from_pkcs8(next_keypair_pkcs8.as_ref())
278 .map_err(|e| RotationError::InvalidKey(e.to_string()))?;
279
280 if !verify_commitment(
282 next_keypair.public_key().as_ref(),
283 &state.next_commitment[0],
284 ) {
285 return Err(RotationError::CommitmentMismatch);
286 }
287
288 let new_current_pub_encoded = format!(
290 "D{}",
291 URL_SAFE_NO_PAD.encode(next_keypair.public_key().as_ref())
292 );
293
294 let (bt, b) = match witness_config {
296 Some(cfg) if cfg.is_enabled() => (
297 cfg.threshold.to_string(),
298 cfg.witness_urls.iter().map(|u| u.to_string()).collect(),
299 ),
300 _ => ("0".to_string(), vec![]),
301 };
302
303 let new_sequence = state.sequence + 1;
305 let mut rot = RotEvent {
306 v: KERI_VERSION.to_string(),
307 d: Said::default(),
308 i: prefix.clone(),
309 s: KeriSequence::new(new_sequence),
310 p: state.last_event_said.clone(),
311 kt: "1".to_string(),
312 k: vec![new_current_pub_encoded], nt: "0".to_string(), n: vec![], bt,
316 b,
317 a: vec![],
318 x: String::new(),
319 };
320
321 let rot_json = serde_json::to_vec(&Event::Rot(rot.clone()))
323 .map_err(|e| RotationError::Serialization(e.to_string()))?;
324 rot.d = compute_said(&rot_json);
325
326 let canonical = super::serialize_for_signing(&Event::Rot(rot.clone()))?;
328 let sig = next_keypair.sign(&canonical);
329 rot.x = URL_SAFE_NO_PAD.encode(sig.as_ref());
330
331 kel.append(&Event::Rot(rot), now)?;
333
334 Ok(new_sequence)
335}
336
337pub fn get_key_state(repo: &Repository, prefix: &Prefix) -> Result<KeyState, RotationError> {
339 let kel = GitKel::new(repo, prefix.as_str());
340 let events = kel.get_events()?;
341 let state = validate_kel(&events)?;
342 Ok(state)
343}
344
345pub fn rotate_keys_with_backend(
356 backend: &impl RegistryBackend,
357 prefix: &Prefix,
358 next_keypair_pkcs8: &Pkcs8Der,
359 _now: chrono::DateTime<chrono::Utc>,
360 _witness_config: Option<&WitnessConfig>,
361) -> Result<RotationResult, RotationError> {
362 let rng = SystemRandom::new();
363
364 let events = collect_events_from_backend(backend, prefix)?;
366 let state = validate_kel(&events)?;
367
368 if !state.can_rotate() {
369 return Err(RotationError::IdentityAbandoned);
370 }
371
372 let next_keypair =
373 crate::identity::helpers::load_keypair_from_der_or_seed(next_keypair_pkcs8.as_ref())
374 .map_err(|e| RotationError::InvalidKey(e.to_string()))?;
375
376 if !verify_commitment(
377 next_keypair.public_key().as_ref(),
378 &state.next_commitment[0],
379 ) {
380 return Err(RotationError::CommitmentMismatch);
381 }
382
383 let new_next_pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng)
384 .map_err(|e| RotationError::KeyGeneration(e.to_string()))?;
385 let new_next_keypair = Ed25519KeyPair::from_pkcs8(new_next_pkcs8.as_ref())
386 .map_err(|e| RotationError::KeyGeneration(e.to_string()))?;
387
388 let new_current_pub_encoded = format!(
389 "D{}",
390 URL_SAFE_NO_PAD.encode(next_keypair.public_key().as_ref())
391 );
392 let new_next_commitment = compute_next_commitment(new_next_keypair.public_key().as_ref());
393
394 let new_sequence = state.sequence + 1;
395 let mut rot = RotEvent {
396 v: KERI_VERSION.to_string(),
397 d: Said::default(),
398 i: prefix.clone(),
399 s: KeriSequence::new(new_sequence),
400 p: state.last_event_said.clone(),
401 kt: "1".to_string(),
402 k: vec![new_current_pub_encoded],
403 nt: "1".to_string(),
404 n: vec![new_next_commitment],
405 bt: "0".to_string(),
406 b: vec![],
407 a: vec![],
408 x: String::new(),
409 };
410
411 let rot_json = serde_json::to_vec(&Event::Rot(rot.clone()))
412 .map_err(|e| RotationError::Serialization(e.to_string()))?;
413 rot.d = compute_said(&rot_json);
414
415 let canonical = super::serialize_for_signing(&Event::Rot(rot.clone()))
416 .map_err(|e| RotationError::Serialization(e.to_string()))?;
417 let sig = next_keypair.sign(&canonical);
418 rot.x = URL_SAFE_NO_PAD.encode(sig.as_ref());
419
420 backend
421 .append_event(prefix, &Event::Rot(rot))
422 .map_err(RotationError::Storage)?;
423
424 Ok(RotationResult {
425 prefix: prefix.clone(),
426 sequence: new_sequence,
427 new_current_keypair_pkcs8: next_keypair_pkcs8.clone(),
428 new_next_keypair_pkcs8: Pkcs8Der::new(new_next_pkcs8.as_ref()),
429 new_current_public_key: next_keypair.public_key().as_ref().to_vec(),
430 new_next_public_key: new_next_keypair.public_key().as_ref().to_vec(),
431 })
432}
433
434pub fn get_key_state_with_backend(
440 backend: &impl RegistryBackend,
441 prefix: &Prefix,
442) -> Result<KeyState, RotationError> {
443 backend
444 .get_key_state(prefix)
445 .map_err(RotationError::Storage)
446}
447
448fn collect_events_from_backend(
449 backend: &impl RegistryBackend,
450 prefix: &Prefix,
451) -> Result<Vec<Event>, RotationError> {
452 let mut events = Vec::new();
453 backend
454 .visit_events(prefix, 0, &mut |e| {
455 events.push(e.clone());
456 ControlFlow::Continue(())
457 })
458 .map_err(RotationError::Storage)?;
459 Ok(events)
460}
461
462#[cfg(test)]
463#[allow(clippy::disallowed_methods)]
464mod tests {
465 use super::*;
466 use crate::keri::create_keri_identity;
467 use tempfile::TempDir;
468
469 fn setup_repo() -> (TempDir, Repository) {
470 let dir = TempDir::new().unwrap();
471 let repo = Repository::init(dir.path()).unwrap();
472
473 let mut config = repo.config().unwrap();
474 config.set_str("user.name", "Test User").unwrap();
475 config.set_str("user.email", "test@example.com").unwrap();
476
477 (dir, repo)
478 }
479
480 #[test]
481 fn rotation_updates_key_and_sequence() {
482 let (_dir, repo) = setup_repo();
483
484 let init = create_keri_identity(&repo, None, chrono::Utc::now()).unwrap();
486
487 let rot = rotate_keys(
489 &repo,
490 &init.prefix,
491 &init.next_keypair_pkcs8,
492 None,
493 chrono::Utc::now(),
494 )
495 .unwrap();
496
497 assert_eq!(rot.prefix, init.prefix);
498 assert_eq!(rot.sequence, 1);
499
500 let kel = GitKel::new(&repo, rot.prefix.as_str());
502 let events = kel.get_events().unwrap();
503 assert_eq!(events.len(), 2);
504 assert!(events[0].is_inception());
505 assert!(events[1].is_rotation());
506 }
507
508 #[test]
509 fn rotation_verifies_commitment() {
510 let (_dir, repo) = setup_repo();
511
512 let init = create_keri_identity(&repo, None, chrono::Utc::now()).unwrap();
513
514 let rng = SystemRandom::new();
516 let wrong_pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
517 let wrong_pkcs8 = Pkcs8Der::new(wrong_pkcs8.as_ref());
518
519 let result = rotate_keys(&repo, &init.prefix, &wrong_pkcs8, None, chrono::Utc::now());
520 assert!(matches!(result, Err(RotationError::CommitmentMismatch)));
521 }
522
523 #[test]
524 fn rotation_chain_works() {
525 let (_dir, repo) = setup_repo();
526
527 let init = create_keri_identity(&repo, None, chrono::Utc::now()).unwrap();
529
530 let rot1 = rotate_keys(
532 &repo,
533 &init.prefix,
534 &init.next_keypair_pkcs8,
535 None,
536 chrono::Utc::now(),
537 )
538 .unwrap();
539 assert_eq!(rot1.sequence, 1);
540
541 let rot2 = rotate_keys(
543 &repo,
544 &init.prefix,
545 &rot1.new_next_keypair_pkcs8,
546 None,
547 chrono::Utc::now(),
548 )
549 .unwrap();
550 assert_eq!(rot2.sequence, 2);
551
552 let kel = GitKel::new(&repo, init.prefix.as_str());
554 let events = kel.get_events().unwrap();
555 assert_eq!(events.len(), 3);
556
557 let state = validate_kel(&events).unwrap();
559 assert_eq!(state.sequence, 2);
560 }
561
562 #[test]
563 fn abandonment_works() {
564 let (_dir, repo) = setup_repo();
565
566 let init = create_keri_identity(&repo, None, chrono::Utc::now()).unwrap();
567
568 let seq = abandon_identity(
570 &repo,
571 &init.prefix,
572 &init.next_keypair_pkcs8,
573 None,
574 chrono::Utc::now(),
575 )
576 .unwrap();
577 assert_eq!(seq, 1);
578
579 let state = get_key_state(&repo, &init.prefix).unwrap();
581 assert!(state.is_abandoned);
582 assert!(!state.can_rotate());
583 }
584
585 #[test]
586 fn abandoned_identity_cannot_rotate() {
587 let (_dir, repo) = setup_repo();
588
589 let init = create_keri_identity(&repo, None, chrono::Utc::now()).unwrap();
590
591 abandon_identity(
593 &repo,
594 &init.prefix,
595 &init.next_keypair_pkcs8,
596 None,
597 chrono::Utc::now(),
598 )
599 .unwrap();
600
601 let rng = SystemRandom::new();
603 let new_pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
604 let new_pkcs8 = Pkcs8Der::new(new_pkcs8.as_ref());
605 let result = rotate_keys(&repo, &init.prefix, &new_pkcs8, None, chrono::Utc::now());
606 assert!(matches!(result, Err(RotationError::IdentityAbandoned)));
607 }
608
609 #[test]
610 fn double_abandonment_fails() {
611 let (_dir, repo) = setup_repo();
612
613 let init = create_keri_identity(&repo, None, chrono::Utc::now()).unwrap();
614
615 abandon_identity(
616 &repo,
617 &init.prefix,
618 &init.next_keypair_pkcs8,
619 None,
620 chrono::Utc::now(),
621 )
622 .unwrap();
623
624 let rng = SystemRandom::new();
626 let new_pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
627 let new_pkcs8 = Pkcs8Der::new(new_pkcs8.as_ref());
628 let result = abandon_identity(&repo, &init.prefix, &new_pkcs8, None, chrono::Utc::now());
629 assert!(matches!(result, Err(RotationError::IdentityAbandoned)));
630 }
631
632 #[test]
633 fn get_key_state_works() {
634 let (_dir, repo) = setup_repo();
635
636 let init = create_keri_identity(&repo, None, chrono::Utc::now()).unwrap();
637
638 let state = get_key_state(&repo, &init.prefix).unwrap();
639 assert_eq!(state.prefix, init.prefix);
640 assert_eq!(state.sequence, 0);
641 assert!(!state.is_abandoned);
642 assert!(state.can_rotate());
643 }
644
645 #[test]
646 fn state_reflects_rotation() {
647 let (_dir, repo) = setup_repo();
648
649 let init = create_keri_identity(&repo, None, chrono::Utc::now()).unwrap();
650 rotate_keys(
651 &repo,
652 &init.prefix,
653 &init.next_keypair_pkcs8,
654 None,
655 chrono::Utc::now(),
656 )
657 .unwrap();
658
659 let state = get_key_state(&repo, &init.prefix).unwrap();
660 assert_eq!(state.sequence, 1);
661
662 let expected_key = format!("D{}", URL_SAFE_NO_PAD.encode(&init.next_public_key));
664 assert_eq!(state.current_keys[0], expected_key);
665 }
666}