1use git2::Repository;
11use ring::rand::SystemRandom;
12use ring::signature::{Ed25519KeyPair, KeyPair};
13use std::path::Path;
14
15use auths_crypto::Pkcs8Der;
16
17use crate::error::InitError;
18use crate::identity::helpers::load_keypair_from_der_or_seed;
19use crate::keri::{
20 CesrKey, Event, GitKel, KeriSequence, Prefix, RotEvent, Said, Threshold, VersionString,
21 rotate_keys, serialize_for_signing, validate_kel,
22};
23use std::sync::Arc;
24
25use crate::storage::layout::StorageLayoutConfig;
26use crate::storage::registry::RegistryBackend;
27use crate::witness_config::WitnessConfig;
28use auths_core::crypto::said::{compute_next_commitment, verify_commitment};
29use auths_core::crypto::signer::{decrypt_keypair, encrypt_keypair};
30use auths_core::signing::PassphraseProvider;
31use auths_core::storage::keychain::{IdentityDID, KeyAlias, KeyRole, KeyStorage};
32use auths_keri::compute_said;
33
34pub struct RotationKeyInfo {
36 pub sequence: u128,
38 pub new_current_pkcs8: Pkcs8Der,
40 pub new_next_pkcs8: Pkcs8Der,
42}
43
44#[derive(Debug, Clone, Default)]
51pub struct RotationShape {
52 pub add_devices: Vec<auths_crypto::CurveType>,
55 pub remove_indices: Vec<u32>,
59 pub new_kt: Option<Threshold>,
61 pub new_nt: Option<Threshold>,
63}
64
65#[allow(clippy::too_many_arguments)]
81pub fn rotate_keri_identity(
82 repo_path: &Path,
83 current_alias: &KeyAlias,
84 next_alias: &KeyAlias,
85 passphrase_provider: &dyn PassphraseProvider,
86 _config: &StorageLayoutConfig,
87 keychain: &(dyn KeyStorage + Send + Sync),
88 witness_config: Option<&WitnessConfig>,
89 now: chrono::DateTime<chrono::Utc>,
90) -> Result<RotationKeyInfo, InitError> {
91 let repo = Repository::open(repo_path)?;
92
93 let (did, _role, _encrypted_current) = keychain.load_key(current_alias)?;
94
95 if keychain.is_hardware_backend() {
96 return Err(InitError::InvalidData(
97 "Rotation requires a software-backed key; current key is hardware-backed \
98 (Secure Enclave). Rotate by initializing a new identity."
99 .into(),
100 ));
101 }
102
103 let prefix = did.as_str().strip_prefix("did:keri:").ok_or_else(|| {
104 InitError::InvalidData(format!("Invalid DID format, expected 'did:keri:': {}", did))
105 })?;
106
107 let kel = GitKel::new(&repo, prefix);
108 let events = kel
109 .get_events()
110 .map_err(|e| InitError::Keri(e.to_string()))?;
111 let state = validate_kel(&events).map_err(|e| InitError::Keri(e.to_string()))?;
112
113 let derived_next_alias = KeyAlias::new_unchecked(format!(
114 "{}--next-{}",
115 current_alias, state.last_establishment_sequence
116 ));
117
118 let (did_check, _role, encrypted_next) = keychain.load_key(&derived_next_alias)?;
119
120 if did != did_check {
121 return Err(InitError::InvalidData(format!(
122 "DID mismatch for pre-committed key '{}': expected {}, found {}",
123 derived_next_alias, did, did_check
124 )));
125 }
126
127 let next_pass = passphrase_provider.get_passphrase(&format!(
128 "Enter passphrase for pre-committed key '{}':",
129 derived_next_alias
130 ))?;
131 let decrypted_next_pkcs8 =
132 Pkcs8Der::new(decrypt_keypair(&encrypted_next, &next_pass)?.to_vec());
133
134 let rotation_result = rotate_keys(
135 &repo,
136 &Prefix::new_unchecked(prefix.to_string()),
137 &decrypted_next_pkcs8,
138 witness_config,
139 now,
140 )
141 .map_err(|e| InitError::Keri(e.to_string()))?;
142
143 let new_pass = passphrase_provider.get_passphrase(&format!(
144 "Create passphrase for new key alias '{}':",
145 next_alias
146 ))?;
147 let confirm_pass =
148 passphrase_provider.get_passphrase(&format!("Confirm passphrase for '{}':", next_alias))?;
149 if new_pass != confirm_pass {
150 return Err(InitError::InvalidData(format!(
151 "Passphrases do not match for alias '{}'",
152 next_alias
153 )));
154 }
155
156 let encrypted_new_current = encrypt_keypair(decrypted_next_pkcs8.as_ref(), &new_pass)?;
157 keychain.store_key(next_alias, &did, KeyRole::Primary, &encrypted_new_current)?;
158
159 let encrypted_future =
161 encrypt_keypair(rotation_result.new_next_keypair_pkcs8.as_ref(), &new_pass)?;
162
163 let future_key_alias =
164 KeyAlias::new_unchecked(format!("{}--next-{}", next_alias, rotation_result.sequence));
165 keychain.store_key(
166 &future_key_alias,
167 &did,
168 KeyRole::NextRotation,
169 &encrypted_future,
170 )?;
171
172 let _ = keychain.delete_key(&derived_next_alias);
173 log::debug!("Cleaned up pre-committed key: {}", derived_next_alias);
174
175 Ok(RotationKeyInfo {
176 sequence: rotation_result.sequence,
177 new_current_pkcs8: rotation_result.new_current_keypair_pkcs8,
178 new_next_pkcs8: rotation_result.new_next_keypair_pkcs8,
179 })
180}
181
182#[allow(clippy::too_many_lines)]
201pub fn rotate_registry_identity(
202 backend: Arc<dyn RegistryBackend + Send + Sync>,
203 current_alias: &KeyAlias,
204 next_alias: &KeyAlias,
205 passphrase_provider: &dyn PassphraseProvider,
206 _config: &StorageLayoutConfig,
207 keychain: &(dyn KeyStorage + Send + Sync),
208 witness_config: Option<&WitnessConfig>,
209) -> Result<RotationKeyInfo, InitError> {
210 let rng = SystemRandom::new();
211
212 let (did, _role, _encrypted_current) = keychain.load_key(current_alias)?;
213
214 if keychain.is_hardware_backend() {
215 return Err(InitError::InvalidData(
216 "Rotation requires a software-backed key; current key is hardware-backed \
217 (Secure Enclave). Rotate by initializing a new identity."
218 .into(),
219 ));
220 }
221
222 let prefix_str = did.as_str().strip_prefix("did:keri:").ok_or_else(|| {
223 InitError::InvalidData(format!("Invalid DID format, expected 'did:keri:': {}", did))
224 })?;
225 let prefix = Prefix::new_unchecked(prefix_str.to_string());
226
227 let state = backend
228 .get_key_state(&prefix)
229 .map_err(|e| InitError::Registry(e.to_string()))?;
230
231 let derived_next_alias = KeyAlias::new_unchecked(format!(
232 "{}--next-{}",
233 current_alias, state.last_establishment_sequence
234 ));
235
236 let (did_check, _role, encrypted_next) = keychain.load_key(&derived_next_alias)?;
237
238 if did != did_check {
239 return Err(InitError::InvalidData(format!(
240 "DID mismatch for pre-committed key '{}': expected {}, found {}",
241 derived_next_alias, did, did_check
242 )));
243 }
244
245 let next_pass = passphrase_provider.get_passphrase(&format!(
246 "Enter passphrase for pre-committed key '{}':",
247 derived_next_alias
248 ))?;
249 let decrypted_next_pkcs8 =
250 Pkcs8Der::new(decrypt_keypair(&encrypted_next, &next_pass)?.to_vec());
251
252 if !state.can_rotate() {
253 return Err(InitError::InvalidData(
254 "Identity is abandoned (cannot rotate)".into(),
255 ));
256 }
257
258 let next_keypair = load_keypair_from_der_or_seed(decrypted_next_pkcs8.as_ref())?;
259
260 #[allow(clippy::expect_used)] let next_verkey = auths_keri::KeriPublicKey::ed25519(next_keypair.public_key().as_ref())
262 .expect("ring Ed25519 public key is 32 bytes");
263 if !verify_commitment(&next_verkey, &state.next_commitment[0]) {
264 return Err(InitError::InvalidData(
265 "Commitment mismatch: next key does not match previous commitment".into(),
266 ));
267 }
268
269 let new_next_pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng)
270 .map_err(|e| InitError::Crypto(format!("Key generation failed: {}", e)))?;
271 let new_next_keypair = Ed25519KeyPair::from_pkcs8(new_next_pkcs8.as_ref())
272 .map_err(|e| InitError::Crypto(format!("Key generation failed: {}", e)))?;
273
274 #[allow(clippy::expect_used)]
275 let new_current_pub_encoded =
277 auths_keri::KeriPublicKey::ed25519(next_keypair.public_key().as_ref())
278 .expect("ring Ed25519 public key is 32 bytes")
279 .to_qb64()
280 .expect("cesride verkey encode is infallible");
281 #[allow(clippy::expect_used)] let new_next_verkey =
283 auths_keri::KeriPublicKey::ed25519(new_next_keypair.public_key().as_ref())
284 .expect("ring Ed25519 public key is 32 bytes");
285 let new_next_commitment = compute_next_commitment(&new_next_verkey);
286
287 let bt = match witness_config {
288 Some(cfg) if cfg.is_enabled() => Threshold::Simple(cfg.threshold as u64),
289 _ => Threshold::Simple(0),
290 };
291
292 let new_sequence = state.sequence + 1;
293 let mut rot = RotEvent {
294 v: VersionString::placeholder(),
295 d: Said::default(),
296 i: prefix.clone(),
297 s: KeriSequence::new(new_sequence),
298 p: state.last_event_said.clone(),
299 kt: Threshold::Simple(1),
300 k: vec![CesrKey::new_unchecked(new_current_pub_encoded)],
301 nt: Threshold::Simple(1),
302 n: vec![new_next_commitment],
303 bt,
304 br: vec![],
305 ba: vec![],
306 c: vec![],
307 a: vec![],
308 };
309
310 let rot_value = serde_json::to_value(Event::Rot(rot.clone()))
311 .map_err(|e| InitError::Keri(format!("Serialization failed: {}", e)))?;
312 rot.d = compute_said(&rot_value)
313 .map_err(|e| InitError::Keri(format!("SAID computation failed: {}", e)))?;
314
315 let canonical = serialize_for_signing(&Event::Rot(rot.clone()))
316 .map_err(|e| InitError::Keri(e.to_string()))?;
317 let sig = next_keypair.sign(&canonical);
318 let attachment = auths_keri::serialize_attachment(&[auths_keri::IndexedSignature {
319 index: 0,
320 prior_index: None,
321 sig: sig.as_ref().to_vec(),
322 }])
323 .map_err(|e| InitError::Keri(format!("attachment serialization: {e}")))?;
324
325 backend
326 .append_signed_event(&prefix, &Event::Rot(rot), &attachment)
327 .map_err(|e| InitError::Registry(e.to_string()))?;
328
329 store_rotated_keys(
330 keychain,
331 passphrase_provider,
332 &did,
333 next_alias,
334 &derived_next_alias,
335 new_sequence,
336 decrypted_next_pkcs8.as_ref(),
337 new_next_pkcs8.as_ref(),
338 )?;
339
340 Ok(RotationKeyInfo {
341 sequence: new_sequence,
342 new_current_pkcs8: decrypted_next_pkcs8,
343 new_next_pkcs8: Pkcs8Der::new(new_next_pkcs8.as_ref()),
344 })
345}
346
347#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
364pub fn rotate_registry_identity_multi(
365 backend: Arc<dyn RegistryBackend + Send + Sync>,
366 current_alias: &KeyAlias,
367 next_alias: &KeyAlias,
368 passphrase_provider: &dyn PassphraseProvider,
369 _config: &StorageLayoutConfig,
370 keychain: &(dyn KeyStorage + Send + Sync),
371 witness_config: Option<&WitnessConfig>,
372 shape: RotationShape,
373) -> Result<RotationKeyInfo, InitError> {
374 let first_cur = KeyAlias::new_unchecked(format!("{}--{}", current_alias, 0));
376 let (did, _role, _encrypted) = keychain
377 .load_key(&first_cur)
378 .or_else(|_| keychain.load_key(current_alias))?;
379
380 if keychain.is_hardware_backend() {
381 return Err(InitError::InvalidData(
382 "Rotation requires software-backed keys; current slot is hardware-backed.".to_string(),
383 ));
384 }
385
386 let prefix_str = did.as_str().strip_prefix("did:keri:").ok_or_else(|| {
387 InitError::InvalidData(format!("Invalid DID format, expected 'did:keri:': {}", did))
388 })?;
389 let prefix = Prefix::new_unchecked(prefix_str.to_string());
390
391 let state = backend
392 .get_key_state(&prefix)
393 .map_err(|e| InitError::Registry(e.to_string()))?;
394
395 if !state.can_rotate() {
396 return Err(InitError::InvalidData(
397 "Identity is abandoned (cannot rotate)".to_string(),
398 ));
399 }
400
401 let prior_key_count = state.current_keys.len();
402
403 let mut remove: Vec<usize> = shape.remove_indices.iter().map(|&i| i as usize).collect();
407 remove.sort_unstable();
408 remove.dedup();
409 if let Some(&bad) = remove.iter().find(|&&i| i >= prior_key_count) {
410 return Err(InitError::InvalidData(format!(
411 "remove index {bad} out of range (prior controller count {prior_key_count})"
412 )));
413 }
414 let surviving_count = prior_key_count - remove.len();
415 if surviving_count == 0 {
416 return Err(InitError::InvalidData(
417 "rotation must retain at least one prior controller to authorise it".to_string(),
418 ));
419 }
420 let new_key_count = surviving_count + shape.add_devices.len();
421
422 let new_kt = shape.new_kt.unwrap_or_else(|| state.threshold.clone());
423 let new_nt = shape.new_nt.unwrap_or_else(|| state.next_threshold.clone());
424
425 crate::keri::inception::validate_threshold_for_key_count(&new_kt, new_key_count)
426 .map_err(|e| InitError::InvalidData(e.to_string()))?;
427 crate::keri::inception::validate_threshold_for_key_count(&new_nt, new_key_count)
428 .map_err(|e| InitError::InvalidData(e.to_string()))?;
429
430 let mut new_current_pkcs8s: Vec<Pkcs8Der> = Vec::with_capacity(new_key_count);
434 let mut new_current_pubs: Vec<auths_keri::KeriPublicKey> = Vec::with_capacity(new_key_count);
435 let mut prior_next_aliases: Vec<KeyAlias> = Vec::with_capacity(prior_key_count);
436 let mut prior_indices: Vec<u32> = Vec::with_capacity(surviving_count);
437
438 let next_pass = passphrase_provider.get_passphrase(&format!(
439 "Enter passphrase for pre-committed keys under alias '{}':",
440 current_alias
441 ))?;
442
443 for idx in 0..prior_key_count {
444 let alias = KeyAlias::new_unchecked(format!(
445 "{}--next-{}-{}",
446 current_alias, state.sequence, idx
447 ));
448 if remove.contains(&idx) {
449 prior_next_aliases.push(alias);
452 continue;
453 }
454 let (did_check, _role, encrypted) = keychain.load_key(&alias)?;
455 if did_check != did {
456 return Err(InitError::InvalidData(format!(
457 "DID mismatch for pre-committed key '{}'",
458 alias
459 )));
460 }
461 let pkcs8 = Pkcs8Der::new(decrypt_keypair(&encrypted, &next_pass)?.to_vec());
462 let keypair = load_keypair_from_der_or_seed(pkcs8.as_ref())?;
463 #[allow(clippy::expect_used)] let next_verkey = auths_keri::KeriPublicKey::ed25519(keypair.public_key().as_ref())
465 .expect("ring Ed25519 public key is 32 bytes");
466 if !verify_commitment(&next_verkey, &state.next_commitment[idx]) {
467 return Err(InitError::InvalidData(format!(
468 "Commitment mismatch at slot {idx}: next key does not match previous commitment"
469 )));
470 }
471 new_current_pubs.push(next_verkey);
472 new_current_pkcs8s.push(pkcs8);
473 prior_indices.push(idx as u32);
474 prior_next_aliases.push(alias);
475 }
476
477 if !shape.add_devices.is_empty() {
480 let added = crate::keri::inception::generate_keypairs_for_init(&shape.add_devices)
481 .map_err(|e| InitError::Crypto(e.to_string()))?;
482 for kp in &added {
483 new_current_pubs.push(kp.verkey());
484 new_current_pkcs8s.push(kp.pkcs8.clone());
485 }
486 }
487
488 let new_next_curves: Vec<auths_crypto::CurveType> = (0..new_key_count)
490 .map(|_| auths_crypto::CurveType::P256)
491 .collect();
492 let new_next_kps = crate::keri::inception::generate_keypairs_for_init(&new_next_curves)
493 .map_err(|e| InitError::Crypto(e.to_string()))?;
494
495 let k: Vec<CesrKey> = new_current_pubs
496 .iter()
497 .map(|vk| {
498 vk.to_qb64()
499 .map(CesrKey::new_unchecked)
500 .map_err(|e| InitError::InvalidData(e.to_string()))
501 })
502 .collect::<Result<_, _>>()?;
503 let n: Vec<Said> = new_next_kps
504 .iter()
505 .map(|kp| compute_next_commitment(&kp.verkey()))
506 .collect();
507
508 let bt = match witness_config {
509 Some(cfg) if cfg.is_enabled() => Threshold::Simple(cfg.threshold as u64),
510 _ => Threshold::Simple(0),
511 };
512
513 let new_sequence = state.sequence + 1;
514 let mut rot = RotEvent {
515 v: VersionString::placeholder(),
516 d: Said::default(),
517 i: prefix.clone(),
518 s: KeriSequence::new(new_sequence),
519 p: state.last_event_said.clone(),
520 kt: new_kt,
521 k,
522 nt: new_nt,
523 n,
524 bt,
525 br: vec![],
526 ba: vec![],
527 c: vec![],
528 a: vec![],
529 };
530
531 let rot_value = serde_json::to_value(Event::Rot(rot.clone()))
532 .map_err(|e| InitError::Keri(format!("Serialization failed: {}", e)))?;
533 rot.d = compute_said(&rot_value)
534 .map_err(|e| InitError::Keri(format!("SAID computation failed: {}", e)))?;
535
536 let canonical = serialize_for_signing(&Event::Rot(rot.clone()))
542 .map_err(|e| InitError::Keri(e.to_string()))?;
543 let signer_keypair = load_keypair_from_der_or_seed(new_current_pkcs8s[0].as_ref())?;
544 let sig = signer_keypair.sign(&canonical);
545 let attachment = auths_keri::serialize_attachment(&[auths_keri::IndexedSignature {
546 index: 0,
547 prior_index: prior_indices.first().copied(),
548 sig: sig.as_ref().to_vec(),
549 }])
550 .map_err(|e| InitError::Keri(format!("attachment serialization: {e}")))?;
551
552 backend
553 .append_signed_event(&prefix, &Event::Rot(rot), &attachment)
554 .map_err(|e| InitError::Registry(e.to_string()))?;
555
556 let new_pass = passphrase_provider.get_passphrase(&format!(
560 "Create passphrase for new key alias '{}':",
561 next_alias
562 ))?;
563 let confirm_pass =
564 passphrase_provider.get_passphrase(&format!("Confirm passphrase for '{}':", next_alias))?;
565 if new_pass != confirm_pass {
566 return Err(InitError::InvalidData(format!(
567 "Passphrases do not match for alias '{}'",
568 next_alias
569 )));
570 }
571
572 for (idx, cur_pkcs8) in new_current_pkcs8s.iter().enumerate() {
573 let slot_alias = KeyAlias::new_unchecked(format!("{}--{}", next_alias, idx));
574 let encrypted = encrypt_keypair(cur_pkcs8.as_ref(), &new_pass)?;
575 keychain.store_key(&slot_alias, &did, KeyRole::Primary, &encrypted)?;
576 }
577 for (idx, nxt_kp) in new_next_kps.iter().enumerate() {
578 let slot_alias =
579 KeyAlias::new_unchecked(format!("{}--next-{}-{}", next_alias, new_sequence, idx));
580 let encrypted = encrypt_keypair(nxt_kp.pkcs8.as_ref(), &new_pass)?;
581 keychain.store_key(&slot_alias, &did, KeyRole::NextRotation, &encrypted)?;
582 }
583 for alias in &prior_next_aliases {
584 let _ = keychain.delete_key(alias);
585 }
586
587 let new_next_pkcs8_bytes = new_next_kps[0].pkcs8.as_ref().to_vec();
591 Ok(RotationKeyInfo {
592 sequence: new_sequence,
593 new_current_pkcs8: new_current_pkcs8s
594 .into_iter()
595 .next()
596 .ok_or_else(|| InitError::Crypto("empty current keyset after rotation".to_string()))?,
597 new_next_pkcs8: Pkcs8Der::new(new_next_pkcs8_bytes),
598 })
599}
600
601#[allow(clippy::too_many_arguments)]
602fn store_rotated_keys(
603 keychain: &(dyn KeyStorage + Send + Sync),
604 passphrase_provider: &dyn PassphraseProvider,
605 did: &IdentityDID,
606 next_alias: &KeyAlias,
607 old_next_alias: &KeyAlias,
608 new_sequence: u128,
609 current_pkcs8: &[u8],
610 new_next_pkcs8: &[u8],
611) -> Result<(), InitError> {
612 let new_pass = passphrase_provider.get_passphrase(&format!(
613 "Create passphrase for new key alias '{}':",
614 next_alias
615 ))?;
616 let confirm_pass =
617 passphrase_provider.get_passphrase(&format!("Confirm passphrase for '{}':", next_alias))?;
618 if new_pass != confirm_pass {
619 return Err(InitError::InvalidData(format!(
620 "Passphrases do not match for alias '{}'",
621 next_alias
622 )));
623 }
624
625 let encrypted_new_current = encrypt_keypair(current_pkcs8, &new_pass)?;
626 keychain.store_key(next_alias, did, KeyRole::Primary, &encrypted_new_current)?;
627
628 let encrypted_future = encrypt_keypair(new_next_pkcs8, &new_pass)?;
630
631 let future_key_alias =
632 KeyAlias::new_unchecked(format!("{}--next-{}", next_alias, new_sequence));
633 keychain.store_key(
634 &future_key_alias,
635 did,
636 KeyRole::NextRotation,
637 &encrypted_future,
638 )?;
639
640 let _ = keychain.delete_key(old_next_alias);
641
642 Ok(())
643}