1use std::ops::ControlFlow;
9
10use auths_crypto::Pkcs8Der;
11use auths_keri::KeriPublicKey;
12
13use auths_core::crypto::said::{compute_next_commitment, verify_commitment};
14use auths_keri::compute_said;
15
16use super::event::{CesrKey, KeriSequence, Threshold, VersionString};
17use super::types::{Prefix, Said};
18use super::{Event, GitKel, KelError, RotEvent, ValidationError, validate_kel};
19use crate::storage::registry::backend::RegistryBackend;
20use crate::witness_config::WitnessConfig;
21
22use super::inception::generate_keypair_for_init;
23
24#[derive(Debug, thiserror::Error)]
26#[non_exhaustive]
27pub enum RotationError {
28 #[error("KEL error: {0}")]
29 Kel(#[from] KelError),
30
31 #[error("Validation error: {0}")]
32 Validation(#[from] ValidationError),
33
34 #[error("Key generation failed: {0}")]
35 KeyGeneration(String),
36
37 #[error("Invalid key: {0}")]
38 InvalidKey(String),
39
40 #[error("Key commitment mismatch")]
41 CommitmentMismatch,
42
43 #[error("Identity is abandoned (empty next commitment)")]
44 IdentityAbandoned,
45
46 #[error("Serialization error: {0}")]
47 Serialization(String),
48
49 #[error("Storage error: {0}")]
50 Storage(#[from] crate::storage::registry::backend::RegistryError),
51
52 #[error("Rotation failed: {0}")]
53 RotationFailed(String),
54
55 #[error("Key not found: {0}")]
56 KeyNotFound(String),
57
58 #[error("Key decryption failed: {0}")]
59 KeyDecryptionFailed(String),
60}
61
62impl From<auths_core::error::AgentError> for RotationError {
63 fn from(e: auths_core::error::AgentError) -> Self {
64 RotationError::RotationFailed(e.to_string())
65 }
66}
67
68impl auths_core::error::AuthsErrorInfo for RotationError {
69 fn error_code(&self) -> &'static str {
70 match self {
71 Self::Kel(_) => "AUTHS-E4821",
72 Self::Validation(_) => "AUTHS-E4822",
73 Self::KeyGeneration(_) => "AUTHS-E4823",
74 Self::InvalidKey(_) => "AUTHS-E4824",
75 Self::CommitmentMismatch => "AUTHS-E4825",
76 Self::IdentityAbandoned => "AUTHS-E4826",
77 Self::Serialization(_) => "AUTHS-E4827",
78 Self::Storage(_) => "AUTHS-E4828",
79 Self::RotationFailed(_) => "AUTHS-E4829",
80 Self::KeyNotFound(_) => "AUTHS-E4830",
81 Self::KeyDecryptionFailed(_) => "AUTHS-E4831",
82 }
83 }
84
85 fn suggestion(&self) -> Option<&'static str> {
86 match self {
87 Self::CommitmentMismatch => Some(
88 "The provided key does not match the pre-committed next key. Use the key that was generated during initialization or the last rotation.",
89 ),
90 Self::IdentityAbandoned => Some(
91 "This identity has been permanently abandoned. Create a new identity with 'auths init'.",
92 ),
93 _ => None,
94 }
95 }
96}
97
98#[derive(Debug, Clone)]
100pub struct RotationResult {
101 pub prefix: Prefix,
102 pub sequence: u128,
103 pub new_current_keypair_pkcs8: Pkcs8Der,
104 pub new_next_keypair_pkcs8: Pkcs8Der,
105 pub new_current_public_key: Vec<u8>,
106 pub new_next_public_key: Vec<u8>,
107}
108
109fn detect_curve_from_state(state: &auths_keri::KeyState) -> auths_crypto::CurveType {
111 state
112 .current_keys
113 .first()
114 .and_then(|k| KeriPublicKey::parse(k.as_str()).ok())
115 .map(|kp| kp.curve())
116 .unwrap_or_default()
117}
118
119fn parse_next_key(
121 pkcs8: &[u8],
122) -> Result<(Vec<u8>, auths_crypto::TypedSeed, String), RotationError> {
123 let parsed = auths_crypto::parse_key_material(pkcs8)
124 .map_err(|e| RotationError::InvalidKey(e.to_string()))?;
125 let cesr_encoded =
126 auths_keri::KeriPublicKey::from_verkey_bytes(&parsed.public_key, parsed.seed.curve())
127 .and_then(|k| k.to_qb64())
128 .map_err(|e| RotationError::InvalidKey(e.to_string()))?;
129 Ok((parsed.public_key, parsed.seed, cesr_encoded))
130}
131
132fn sign_rotation(
134 seed: &auths_crypto::TypedSeed,
135 canonical: &[u8],
136) -> Result<Vec<u8>, RotationError> {
137 auths_crypto::typed_sign(seed, canonical)
138 .map_err(|e| RotationError::Serialization(format!("signing failed: {e}")))
139}
140
141pub fn rotate_keys(
143 repo: &git2::Repository,
144 prefix: &Prefix,
145 next_keypair_pkcs8: &Pkcs8Der,
146 witness_config: Option<&WitnessConfig>,
147 now: chrono::DateTime<chrono::Utc>,
148) -> Result<RotationResult, RotationError> {
149 let kel = GitKel::new(repo, prefix.as_str());
150 let events = kel.get_events()?;
151 let state = validate_kel(&events)?;
152
153 if !state.can_rotate() {
154 return Err(RotationError::IdentityAbandoned);
155 }
156
157 let curve = detect_curve_from_state(&state);
158 let (next_pub_bytes, next_seed, next_cesr) = parse_next_key(next_keypair_pkcs8.as_ref())?;
159
160 #[allow(clippy::expect_used)]
161 let next_verkey =
163 auths_keri::KeriPublicKey::parse(&next_cesr).expect("valid CESR from parse_next_key");
164 if !verify_commitment(&next_verkey, &state.next_commitment[0]) {
165 return Err(RotationError::CommitmentMismatch);
166 }
167
168 let new_next = generate_keypair_for_init(curve)
169 .map_err(|e| RotationError::KeyGeneration(e.to_string()))?;
170
171 let new_next_commitment = compute_next_commitment(&new_next.verkey());
172
173 let bt = match witness_config {
174 Some(cfg) if cfg.is_enabled() => Threshold::Simple(cfg.threshold as u64),
175 _ => Threshold::Simple(0),
176 };
177
178 let new_sequence = state.sequence + 1;
179 let mut rot = RotEvent {
180 v: VersionString::placeholder(),
181 d: Said::default(),
182 i: prefix.clone(),
183 s: KeriSequence::new(new_sequence),
184 p: state.last_event_said.clone(),
185 kt: Threshold::Simple(1),
186 k: vec![CesrKey::new_unchecked(next_cesr.clone())],
187 nt: Threshold::Simple(1),
188 n: vec![new_next_commitment],
189 bt,
190 br: vec![],
191 ba: vec![],
192 c: vec![],
193 a: vec![],
194 };
195
196 let rot_value = serde_json::to_value(Event::Rot(rot.clone()))
197 .map_err(|e| RotationError::Serialization(e.to_string()))?;
198 rot.d = compute_said(&rot_value).map_err(|e| RotationError::Serialization(e.to_string()))?;
199
200 let canonical = super::serialize_for_signing(&Event::Rot(rot.clone()))?;
201 let _sig = sign_rotation(&next_seed, &canonical)?;
202
203 kel.append(&Event::Rot(rot.clone()), now)?;
204
205 #[cfg(feature = "witness-client")]
206 if let Some(config) = witness_config
207 && config.is_enabled()
208 {
209 let canonical_for_witness = super::serialize_for_signing(&Event::Rot(rot.clone()))?;
210 super::witness_integration::collect_and_store_receipts(
211 repo.path().parent().unwrap_or(repo.path()),
212 prefix,
213 &rot.d,
214 &canonical_for_witness,
215 config,
216 now,
217 )
218 .map_err(|e| RotationError::Serialization(e.to_string()))?;
219 }
220
221 Ok(RotationResult {
222 prefix: prefix.clone(),
223 sequence: new_sequence,
224 new_current_keypair_pkcs8: next_keypair_pkcs8.clone(),
225 new_next_keypair_pkcs8: new_next.pkcs8,
226 new_current_public_key: next_pub_bytes,
227 new_next_public_key: new_next.public_key,
228 })
229}
230
231pub fn abandon_identity(
233 repo: &git2::Repository,
234 prefix: &Prefix,
235 next_keypair_pkcs8: &Pkcs8Der,
236 witness_config: Option<&WitnessConfig>,
237 now: chrono::DateTime<chrono::Utc>,
238) -> Result<u128, RotationError> {
239 let kel = GitKel::new(repo, prefix.as_str());
240 let events = kel.get_events()?;
241 let state = validate_kel(&events)?;
242
243 if state.is_abandoned {
244 return Err(RotationError::IdentityAbandoned);
245 }
246
247 let (_next_pub_bytes, next_seed, next_cesr) = parse_next_key(next_keypair_pkcs8.as_ref())?;
248
249 #[allow(clippy::expect_used)]
250 let next_verkey =
252 auths_keri::KeriPublicKey::parse(&next_cesr).expect("valid CESR from parse_next_key");
253 if !verify_commitment(&next_verkey, &state.next_commitment[0]) {
254 return Err(RotationError::CommitmentMismatch);
255 }
256
257 let bt = match witness_config {
258 Some(cfg) if cfg.is_enabled() => Threshold::Simple(cfg.threshold as u64),
259 _ => Threshold::Simple(0),
260 };
261
262 let new_sequence = state.sequence + 1;
263 let mut rot = RotEvent {
264 v: VersionString::placeholder(),
265 d: Said::default(),
266 i: prefix.clone(),
267 s: KeriSequence::new(new_sequence),
268 p: state.last_event_said.clone(),
269 kt: Threshold::Simple(1),
270 k: vec![CesrKey::new_unchecked(next_cesr.clone())],
271 nt: Threshold::Simple(0),
272 n: vec![],
273 bt,
274 br: vec![],
275 ba: vec![],
276 c: vec![],
277 a: vec![],
278 };
279
280 let rot_value = serde_json::to_value(Event::Rot(rot.clone()))
281 .map_err(|e| RotationError::Serialization(e.to_string()))?;
282 rot.d = compute_said(&rot_value).map_err(|e| RotationError::Serialization(e.to_string()))?;
283
284 let canonical = super::serialize_for_signing(&Event::Rot(rot.clone()))?;
285 let _sig = sign_rotation(&next_seed, &canonical)?;
286
287 kel.append(&Event::Rot(rot), now)?;
288
289 Ok(new_sequence)
290}
291
292pub fn get_key_state(
294 repo: &git2::Repository,
295 prefix: &Prefix,
296) -> Result<auths_keri::KeyState, RotationError> {
297 let kel = GitKel::new(repo, prefix.as_str());
298 let events = kel.get_events()?;
299 let state = validate_kel(&events)?;
300 Ok(state)
301}
302
303pub fn rotate_keys_with_backend(
305 backend: &impl RegistryBackend,
306 prefix: &Prefix,
307 next_keypair_pkcs8: &Pkcs8Der,
308 _now: chrono::DateTime<chrono::Utc>,
309 _witness_config: Option<&WitnessConfig>,
310) -> Result<RotationResult, RotationError> {
311 let events = collect_events_from_backend(backend, prefix)?;
312 let state = validate_kel(&events)?;
313
314 if !state.can_rotate() {
315 return Err(RotationError::IdentityAbandoned);
316 }
317
318 let curve = detect_curve_from_state(&state);
319 let (next_pub_bytes, next_seed, next_cesr) = parse_next_key(next_keypair_pkcs8.as_ref())?;
320
321 #[allow(clippy::expect_used)]
322 let next_verkey =
324 auths_keri::KeriPublicKey::parse(&next_cesr).expect("valid CESR from parse_next_key");
325 if !verify_commitment(&next_verkey, &state.next_commitment[0]) {
326 return Err(RotationError::CommitmentMismatch);
327 }
328
329 let new_next = generate_keypair_for_init(curve)
330 .map_err(|e| RotationError::KeyGeneration(e.to_string()))?;
331
332 let new_next_commitment = compute_next_commitment(&new_next.verkey());
333
334 let new_sequence = state.sequence + 1;
335 let mut rot = RotEvent {
336 v: VersionString::placeholder(),
337 d: Said::default(),
338 i: prefix.clone(),
339 s: KeriSequence::new(new_sequence),
340 p: state.last_event_said.clone(),
341 kt: Threshold::Simple(1),
342 k: vec![CesrKey::new_unchecked(next_cesr.clone())],
343 nt: Threshold::Simple(1),
344 n: vec![new_next_commitment],
345 bt: Threshold::Simple(0),
346 br: vec![],
347 ba: vec![],
348 c: vec![],
349 a: vec![],
350 };
351
352 let rot_value = serde_json::to_value(Event::Rot(rot.clone()))
353 .map_err(|e| RotationError::Serialization(e.to_string()))?;
354 rot.d = compute_said(&rot_value).map_err(|e| RotationError::Serialization(e.to_string()))?;
355
356 let canonical = super::serialize_for_signing(&Event::Rot(rot.clone()))
357 .map_err(|e| RotationError::Serialization(e.to_string()))?;
358 let sig = sign_rotation(&next_seed, &canonical)?;
359 let attachment = auths_keri::serialize_attachment(&[auths_keri::IndexedSignature {
360 index: 0,
361 prior_index: None,
362 sig,
363 }])
364 .map_err(|e| RotationError::Serialization(e.to_string()))?;
365
366 backend
367 .append_signed_event(prefix, &Event::Rot(rot), &attachment)
368 .map_err(RotationError::Storage)?;
369
370 Ok(RotationResult {
371 prefix: prefix.clone(),
372 sequence: new_sequence,
373 new_current_keypair_pkcs8: next_keypair_pkcs8.clone(),
374 new_next_keypair_pkcs8: new_next.pkcs8,
375 new_current_public_key: next_pub_bytes,
376 new_next_public_key: new_next.public_key,
377 })
378}
379
380pub fn get_key_state_with_backend(
382 backend: &impl RegistryBackend,
383 prefix: &Prefix,
384) -> Result<auths_keri::KeyState, RotationError> {
385 let events = collect_events_from_backend(backend, prefix)?;
386 let state = validate_kel(&events)?;
387 Ok(state)
388}
389
390fn collect_events_from_backend(
391 backend: &impl RegistryBackend,
392 prefix: &Prefix,
393) -> Result<Vec<Event>, RotationError> {
394 let mut events = Vec::new();
395 backend
396 .visit_events(prefix, 0, &mut |e| {
397 events.push(e.clone());
398 ControlFlow::Continue(())
399 })
400 .map_err(RotationError::Storage)?;
401
402 if events.is_empty() {
403 return Err(RotationError::Kel(KelError::NotFound(
404 prefix.as_str().to_string(),
405 )));
406 }
407 Ok(events)
408}
409
410#[cfg(test)]
411#[allow(clippy::unwrap_used, clippy::disallowed_methods)]
412mod tests {
413 use super::*;
414 use crate::keri::create_keri_identity_with_curve;
415
416 #[test]
417 fn rotation_updates_key_and_sequence() {
418 let (_dir, repo) = auths_test_utils::git::init_test_repo();
419 let init = create_keri_identity_with_curve(
420 &repo,
421 None,
422 chrono::Utc::now(),
423 auths_crypto::CurveType::Ed25519,
424 )
425 .unwrap();
426
427 let rot = rotate_keys(
428 &repo,
429 &init.prefix,
430 &init.next_keypair_pkcs8,
431 None,
432 chrono::Utc::now(),
433 )
434 .unwrap();
435 assert_eq!(rot.sequence, 1);
436 assert_ne!(rot.new_current_public_key, init.current_public_key);
437 }
438
439 #[test]
440 fn rotation_verifies_commitment() {
441 let (_dir, repo) = auths_test_utils::git::init_test_repo();
442 let init = create_keri_identity_with_curve(
443 &repo,
444 None,
445 chrono::Utc::now(),
446 auths_crypto::CurveType::Ed25519,
447 )
448 .unwrap();
449
450 let wrong_key = Pkcs8Der::new([99u8; 85].to_vec());
451 let result = rotate_keys(&repo, &init.prefix, &wrong_key, None, chrono::Utc::now());
452 assert!(result.is_err());
453 }
454
455 #[test]
456 fn rotation_chain_works() {
457 let (_dir, repo) = auths_test_utils::git::init_test_repo();
458 let init = create_keri_identity_with_curve(
459 &repo,
460 None,
461 chrono::Utc::now(),
462 auths_crypto::CurveType::Ed25519,
463 )
464 .unwrap();
465
466 let rot1 = rotate_keys(
467 &repo,
468 &init.prefix,
469 &init.next_keypair_pkcs8,
470 None,
471 chrono::Utc::now(),
472 )
473 .unwrap();
474 assert_eq!(rot1.sequence, 1);
475
476 let rot2 = rotate_keys(
477 &repo,
478 &init.prefix,
479 &rot1.new_next_keypair_pkcs8,
480 None,
481 chrono::Utc::now(),
482 )
483 .unwrap();
484 assert_eq!(rot2.sequence, 2);
485 }
486
487 #[test]
488 fn state_reflects_rotation() {
489 let (_dir, repo) = auths_test_utils::git::init_test_repo();
490 let init = create_keri_identity_with_curve(
491 &repo,
492 None,
493 chrono::Utc::now(),
494 auths_crypto::CurveType::Ed25519,
495 )
496 .unwrap();
497
498 let _rot = rotate_keys(
499 &repo,
500 &init.prefix,
501 &init.next_keypair_pkcs8,
502 None,
503 chrono::Utc::now(),
504 )
505 .unwrap();
506 let state = get_key_state(&repo, &init.prefix).unwrap();
507 assert_eq!(state.sequence, 1);
508 }
509
510 #[test]
511 fn abandonment_works() {
512 let (_dir, repo) = auths_test_utils::git::init_test_repo();
513 let init = create_keri_identity_with_curve(
514 &repo,
515 None,
516 chrono::Utc::now(),
517 auths_crypto::CurveType::Ed25519,
518 )
519 .unwrap();
520
521 let seq = abandon_identity(
522 &repo,
523 &init.prefix,
524 &init.next_keypair_pkcs8,
525 None,
526 chrono::Utc::now(),
527 )
528 .unwrap();
529 assert_eq!(seq, 1);
530
531 let state = get_key_state(&repo, &init.prefix).unwrap();
532 assert!(state.is_abandoned);
533 }
534
535 #[test]
536 fn abandoned_identity_cannot_rotate() {
537 let (_dir, repo) = auths_test_utils::git::init_test_repo();
538 let init = create_keri_identity_with_curve(
539 &repo,
540 None,
541 chrono::Utc::now(),
542 auths_crypto::CurveType::Ed25519,
543 )
544 .unwrap();
545
546 abandon_identity(
547 &repo,
548 &init.prefix,
549 &init.next_keypair_pkcs8,
550 None,
551 chrono::Utc::now(),
552 )
553 .unwrap();
554
555 let wrong = Pkcs8Der::new(vec![1, 2, 3]);
556 let result = rotate_keys(&repo, &init.prefix, &wrong, None, chrono::Utc::now());
557 assert!(matches!(result, Err(RotationError::IdentityAbandoned)));
558 }
559
560 #[test]
561 fn double_abandonment_fails() {
562 let (_dir, repo) = auths_test_utils::git::init_test_repo();
563 let init = create_keri_identity_with_curve(
564 &repo,
565 None,
566 chrono::Utc::now(),
567 auths_crypto::CurveType::Ed25519,
568 )
569 .unwrap();
570
571 abandon_identity(
572 &repo,
573 &init.prefix,
574 &init.next_keypair_pkcs8,
575 None,
576 chrono::Utc::now(),
577 )
578 .unwrap();
579 let result = abandon_identity(
580 &repo,
581 &init.prefix,
582 &init.next_keypair_pkcs8,
583 None,
584 chrono::Utc::now(),
585 );
586 assert!(matches!(result, Err(RotationError::IdentityAbandoned)));
587 }
588
589 #[test]
590 fn p256_rotation_works() {
591 let (_dir, repo) = auths_test_utils::git::init_test_repo();
592 let init = create_keri_identity_with_curve(
593 &repo,
594 None,
595 chrono::Utc::now(),
596 auths_crypto::CurveType::P256,
597 )
598 .unwrap();
599
600 let rot = rotate_keys(
601 &repo,
602 &init.prefix,
603 &init.next_keypair_pkcs8,
604 None,
605 chrono::Utc::now(),
606 )
607 .unwrap();
608 assert_eq!(rot.sequence, 1);
609 assert_eq!(rot.new_current_public_key.len(), 33);
610 assert_eq!(rot.new_next_public_key.len(), 33);
611 }
612
613 #[test]
614 fn p256_rotation_chain_works() {
615 let (_dir, repo) = auths_test_utils::git::init_test_repo();
616 let init = create_keri_identity_with_curve(
617 &repo,
618 None,
619 chrono::Utc::now(),
620 auths_crypto::CurveType::P256,
621 )
622 .unwrap();
623
624 let rot1 = rotate_keys(
625 &repo,
626 &init.prefix,
627 &init.next_keypair_pkcs8,
628 None,
629 chrono::Utc::now(),
630 )
631 .unwrap();
632 let rot2 = rotate_keys(
633 &repo,
634 &init.prefix,
635 &rot1.new_next_keypair_pkcs8,
636 None,
637 chrono::Utc::now(),
638 )
639 .unwrap();
640 assert_eq!(rot2.sequence, 2);
641 assert_eq!(rot2.new_next_public_key.len(), 33);
642 }
643}