1use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
9use ring::rand::SystemRandom;
10use ring::signature::{Ed25519KeyPair, KeyPair};
11use zeroize::Zeroizing;
12
13use auths_core::crypto::said::{compute_next_commitment, compute_said, verify_commitment};
14use auths_core::crypto::signer::{decrypt_keypair, encrypt_keypair, load_seed_and_pubkey};
15use auths_core::ports::clock::ClockProvider;
16use auths_core::storage::keychain::{IdentityDID, KeyAlias, KeyStorage, extract_public_key_bytes};
17use auths_id::identity::helpers::{
18 ManagedIdentity, encode_seed_as_pkcs8, extract_seed_bytes, load_keypair_from_der_or_seed,
19};
20use auths_id::keri::{
21 Event, KERI_VERSION, KeriSequence, KeyState, Prefix, RotEvent, Said, serialize_for_signing,
22};
23use auths_id::ports::registry::RegistryBackend;
24use auths_id::witness_config::WitnessConfig;
25
26use crate::context::AuthsContext;
27use crate::error::RotationError;
28use crate::result::RotationResult;
29use crate::types::RotationConfig;
30
31pub fn compute_rotation_event(
51 state: &KeyState,
52 next_keypair: &Ed25519KeyPair,
53 new_next_keypair: &Ed25519KeyPair,
54 witness_config: Option<&WitnessConfig>,
55) -> Result<(RotEvent, Vec<u8>), RotationError> {
56 let prefix = &state.prefix;
57
58 let new_current_pub_encoded = format!(
59 "D{}",
60 URL_SAFE_NO_PAD.encode(next_keypair.public_key().as_ref())
61 );
62 let new_next_commitment = compute_next_commitment(new_next_keypair.public_key().as_ref());
63
64 let (bt, b) = match witness_config {
65 Some(cfg) if cfg.is_enabled() => (
66 cfg.threshold.to_string(),
67 cfg.witness_urls.iter().map(|u| u.to_string()).collect(),
68 ),
69 _ => ("0".to_string(), vec![]),
70 };
71
72 let new_sequence = state.sequence + 1;
73 let mut rot = RotEvent {
74 v: KERI_VERSION.to_string(),
75 d: Said::default(),
76 i: prefix.clone(),
77 s: KeriSequence::new(new_sequence),
78 p: state.last_event_said.clone(),
79 kt: "1".to_string(),
80 k: vec![new_current_pub_encoded],
81 nt: "1".to_string(),
82 n: vec![new_next_commitment],
83 bt,
84 b,
85 a: vec![],
86 x: String::new(),
87 };
88
89 let rot_json = serde_json::to_vec(&Event::Rot(rot.clone()))
90 .map_err(|e| RotationError::RotationFailed(format!("serialization failed: {e}")))?;
91 rot.d = compute_said(&rot_json);
92
93 let canonical = serialize_for_signing(&Event::Rot(rot.clone()))
94 .map_err(|e| RotationError::RotationFailed(format!("serialize for signing failed: {e}")))?;
95 let sig = next_keypair.sign(&canonical);
96 rot.x = URL_SAFE_NO_PAD.encode(sig.as_ref());
97
98 let event_bytes = serialize_for_signing(&Event::Rot(rot.clone()))
99 .map_err(|e| RotationError::RotationFailed(format!("final serialization failed: {e}")))?;
100
101 Ok((rot, event_bytes))
102}
103
104pub struct RotationKeyMaterial {
106 pub did: IdentityDID,
108 pub next_alias: KeyAlias,
110 pub new_next_alias: KeyAlias,
112 pub old_next_alias: KeyAlias,
114 pub new_current_encrypted: Vec<u8>,
116 pub new_next_encrypted: Vec<u8>,
118}
119
120pub fn apply_rotation(
141 rot: &RotEvent,
142 prefix: &Prefix,
143 key_material: RotationKeyMaterial,
144 registry: &(dyn RegistryBackend + Send + Sync),
145 key_storage: &(dyn KeyStorage + Send + Sync),
146) -> Result<(), RotationError> {
147 registry
148 .append_event(prefix, &Event::Rot(rot.clone()))
149 .map_err(|e| RotationError::RotationFailed(format!("KEL append failed: {e}")))?;
150
151 let keychain_result = (|| {
154 key_storage
155 .store_key(
156 &key_material.next_alias,
157 &key_material.did,
158 &key_material.new_current_encrypted,
159 )
160 .map_err(|e| e.to_string())?;
161
162 key_storage
163 .store_key(
164 &key_material.new_next_alias,
165 &key_material.did,
166 &key_material.new_next_encrypted,
167 )
168 .map_err(|e| e.to_string())?;
169
170 let _ = key_storage.delete_key(&key_material.old_next_alias);
171
172 Ok::<(), String>(())
173 })();
174
175 keychain_result.map_err(RotationError::PartialRotation)
176}
177
178pub fn rotate_identity(
199 config: RotationConfig,
200 ctx: &AuthsContext,
201 clock: &dyn ClockProvider,
202) -> Result<RotationResult, RotationError> {
203 let (identity, prefix, current_alias) = resolve_rotation_context(&config, ctx)?;
204 let next_alias = config.next_key_alias.unwrap_or_else(|| {
205 KeyAlias::new_unchecked(format!(
206 "{}-rotated-{}",
207 current_alias,
208 clock.now().format("%Y%m%d%H%M%S")
209 ))
210 });
211
212 let previous_key_fingerprint = extract_previous_fingerprint(ctx, ¤t_alias)?;
213
214 let state = ctx
215 .registry
216 .get_key_state(&prefix)
217 .map_err(|e| RotationError::KelHistoryFailed(e.to_string()))?;
218
219 let (decrypted_next_pkcs8, old_next_alias) =
220 retrieve_precommitted_key(&identity.controller_did, ¤t_alias, &state, ctx)?;
221
222 let (rot, new_next_pkcs8) = generate_rotation_keys(&identity, &state, &decrypted_next_pkcs8)?;
223
224 finalize_rotation_storage(
225 FinalizeParams {
226 did: &identity.controller_did,
227 prefix: &prefix,
228 next_alias: &next_alias,
229 old_next_alias: &old_next_alias,
230 current_pkcs8: &decrypted_next_pkcs8,
231 new_next_pkcs8: new_next_pkcs8.as_ref(),
232 rot: &rot,
233 state: &state,
234 },
235 ctx,
236 )?;
237
238 let (_, new_pubkey) = load_seed_and_pubkey(&decrypted_next_pkcs8)
239 .map_err(|e| RotationError::RotationFailed(e.to_string()))?;
240
241 Ok(RotationResult {
242 controller_did: identity.controller_did,
243 new_key_fingerprint: hex::encode(&new_pubkey[..8]),
244 previous_key_fingerprint,
245 })
246}
247
248fn resolve_rotation_context(
250 config: &RotationConfig,
251 ctx: &AuthsContext,
252) -> Result<(ManagedIdentity, Prefix, KeyAlias), RotationError> {
253 let identity =
254 ctx.identity_storage
255 .load_identity()
256 .map_err(|_| RotationError::IdentityNotFound {
257 path: config.repo_path.clone(),
258 })?;
259
260 let prefix_str = identity
261 .controller_did
262 .as_str()
263 .strip_prefix("did:keri:")
264 .ok_or_else(|| {
265 RotationError::RotationFailed(format!(
266 "invalid DID format, expected 'did:keri:': {}",
267 identity.controller_did
268 ))
269 })?;
270 let prefix = Prefix::new_unchecked(prefix_str.to_string());
271
272 let current_alias = match &config.identity_key_alias {
273 Some(alias) => alias.clone(),
274 None => {
275 let aliases = ctx
276 .key_storage
277 .list_aliases_for_identity(&identity.controller_did)
278 .map_err(|e| RotationError::RotationFailed(format!("alias lookup failed: {e}")))?;
279 aliases
280 .into_iter()
281 .find(|a| !a.contains("--next-"))
282 .ok_or_else(|| {
283 RotationError::KeyNotFound(format!(
284 "no active signing key for {}",
285 identity.controller_did
286 ))
287 })?
288 }
289 };
290
291 Ok((identity, prefix, current_alias))
292}
293
294fn extract_previous_fingerprint(
295 ctx: &AuthsContext,
296 current_alias: &KeyAlias,
297) -> Result<String, RotationError> {
298 let old_pubkey_bytes = extract_public_key_bytes(
299 ctx.key_storage.as_ref(),
300 current_alias,
301 ctx.passphrase_provider.as_ref(),
302 )
303 .map_err(|e| RotationError::KeyDecryptionFailed(e.to_string()))?;
304
305 Ok(hex::encode(&old_pubkey_bytes[..8]))
306}
307
308fn retrieve_precommitted_key(
310 did: &IdentityDID,
311 current_alias: &KeyAlias,
312 state: &KeyState,
313 ctx: &AuthsContext,
314) -> Result<(Zeroizing<Vec<u8>>, KeyAlias), RotationError> {
315 let target_alias =
316 KeyAlias::new_unchecked(format!("{}--next-{}", current_alias, state.sequence));
317
318 let (did_check, encrypted_next) = ctx.key_storage.load_key(&target_alias).map_err(|e| {
319 RotationError::KeyNotFound(format!(
320 "pre-committed next key '{}' not found: {e}",
321 target_alias
322 ))
323 })?;
324
325 if did != &did_check {
326 return Err(RotationError::RotationFailed(format!(
327 "DID mismatch for pre-committed key '{}': expected {}, found {}",
328 target_alias, did, did_check
329 )));
330 }
331
332 let pass = ctx
333 .passphrase_provider
334 .get_passphrase(&format!(
335 "Enter passphrase for pre-committed key '{}':",
336 target_alias
337 ))
338 .map_err(|e| RotationError::KeyDecryptionFailed(e.to_string()))?;
339
340 let decrypted = decrypt_keypair(&encrypted_next, &pass)
341 .map_err(|e| RotationError::KeyDecryptionFailed(e.to_string()))?;
342
343 let keypair = load_keypair_from_der_or_seed(&decrypted)
344 .map_err(|e| RotationError::KeyDecryptionFailed(e.to_string()))?;
345
346 if !verify_commitment(keypair.public_key().as_ref(), &state.next_commitment[0]) {
347 return Err(RotationError::RotationFailed(
348 "commitment mismatch: next key does not match previous commitment".into(),
349 ));
350 }
351
352 Ok((decrypted, target_alias))
353}
354
355fn generate_rotation_keys(
357 identity: &ManagedIdentity,
358 state: &KeyState,
359 current_key_pkcs8: &[u8],
360) -> Result<(RotEvent, ring::pkcs8::Document), RotationError> {
361 let witness_config: Option<WitnessConfig> = identity
362 .metadata
363 .as_ref()
364 .and_then(|m| m.get("witness_config"))
365 .and_then(|wc| serde_json::from_value(wc.clone()).ok());
366
367 let rng = SystemRandom::new();
368 let new_next_pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng)
369 .map_err(|e| RotationError::RotationFailed(format!("key generation failed: {e}")))?;
370 let new_next_keypair = Ed25519KeyPair::from_pkcs8(new_next_pkcs8.as_ref())
371 .map_err(|e| RotationError::RotationFailed(format!("key construction failed: {e}")))?;
372
373 let next_keypair = load_keypair_from_der_or_seed(current_key_pkcs8)
374 .map_err(|e| RotationError::KeyDecryptionFailed(e.to_string()))?;
375
376 let (rot, _event_bytes) = compute_rotation_event(
377 state,
378 &next_keypair,
379 &new_next_keypair,
380 witness_config.as_ref(),
381 )?;
382
383 Ok((rot, new_next_pkcs8))
384}
385
386struct FinalizeParams<'a> {
387 did: &'a IdentityDID,
388 prefix: &'a Prefix,
389 next_alias: &'a KeyAlias,
390 old_next_alias: &'a KeyAlias,
391 current_pkcs8: &'a [u8],
392 new_next_pkcs8: &'a [u8],
393 rot: &'a RotEvent,
394 state: &'a KeyState,
395}
396
397fn finalize_rotation_storage(
399 params: FinalizeParams<'_>,
400 ctx: &AuthsContext,
401) -> Result<(), RotationError> {
402 let new_pass = ctx
403 .passphrase_provider
404 .get_passphrase(&format!(
405 "Create passphrase for new key alias '{}':",
406 params.next_alias
407 ))
408 .map_err(|e| RotationError::KeyDecryptionFailed(e.to_string()))?;
409
410 let confirm_pass = ctx
411 .passphrase_provider
412 .get_passphrase(&format!("Confirm passphrase for '{}':", params.next_alias))
413 .map_err(|e| RotationError::KeyDecryptionFailed(e.to_string()))?;
414
415 if new_pass != confirm_pass {
416 return Err(RotationError::RotationFailed(format!(
417 "passphrases do not match for alias '{}'",
418 params.next_alias
419 )));
420 }
421
422 let encrypted_new_current = encrypt_keypair(params.current_pkcs8, &new_pass)
423 .map_err(|e| RotationError::RotationFailed(format!("encrypt new current key: {e}")))?;
424
425 let new_next_seed = extract_seed_bytes(params.new_next_pkcs8)
426 .map_err(|e| RotationError::RotationFailed(format!("extract new next seed: {e}")))?;
427 let new_next_seed_pkcs8 = encode_seed_as_pkcs8(new_next_seed)
428 .map_err(|e| RotationError::RotationFailed(format!("encode new next seed: {e}")))?;
429 let encrypted_new_next = encrypt_keypair(&new_next_seed_pkcs8, &new_pass)
430 .map_err(|e| RotationError::RotationFailed(format!("encrypt new next key: {e}")))?;
431
432 let new_sequence = params.state.sequence + 1;
433 let new_next_alias =
434 KeyAlias::new_unchecked(format!("{}--next-{}", params.next_alias, new_sequence));
435
436 let key_material = RotationKeyMaterial {
437 did: params.did.clone(),
438 next_alias: params.next_alias.clone(),
439 new_next_alias,
440 old_next_alias: params.old_next_alias.clone(),
441 new_current_encrypted: encrypted_new_current.to_vec(),
442 new_next_encrypted: encrypted_new_next.to_vec(),
443 };
444
445 apply_rotation(
446 params.rot,
447 params.prefix,
448 key_material,
449 ctx.registry.as_ref(),
450 ctx.key_storage.as_ref(),
451 )
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457 use std::sync::Arc;
458
459 use auths_core::PrefilledPassphraseProvider;
460 use auths_core::ports::clock::SystemClock;
461 use auths_core::signing::{PassphraseProvider, StorageSigner};
462 use auths_core::storage::memory::{MEMORY_KEYCHAIN, MemoryKeychainHandle};
463 use auths_id::attestation::export::AttestationSink;
464 use auths_id::ports::registry::RegistryBackend;
465 use auths_id::storage::attestation::AttestationSource;
466 use auths_id::storage::identity::IdentityStorage;
467 use auths_id::testing::fakes::FakeIdentityStorage;
468 use auths_id::testing::fakes::FakeRegistryBackend;
469 use auths_id::testing::fakes::{FakeAttestationSink, FakeAttestationSource};
470
471 use crate::setup::setup_developer;
472 use crate::types::{DeveloperSetupConfig, GitSigningScope};
473
474 fn fake_ctx(passphrase: &str) -> AuthsContext {
475 MEMORY_KEYCHAIN.lock().unwrap().clear_all().ok();
476 AuthsContext::builder()
477 .registry(
478 Arc::new(FakeRegistryBackend::new()) as Arc<dyn RegistryBackend + Send + Sync>
479 )
480 .key_storage(Arc::new(MemoryKeychainHandle))
481 .clock(Arc::new(SystemClock))
482 .identity_storage(
483 Arc::new(FakeIdentityStorage::new()) as Arc<dyn IdentityStorage + Send + Sync>
484 )
485 .attestation_sink(
486 Arc::new(FakeAttestationSink::new()) as Arc<dyn AttestationSink + Send + Sync>
487 )
488 .attestation_source(
489 Arc::new(FakeAttestationSource::new())
490 as Arc<dyn AttestationSource + Send + Sync>,
491 )
492 .passphrase_provider(
493 Arc::new(PrefilledPassphraseProvider::new(passphrase))
494 as Arc<dyn PassphraseProvider + Send + Sync>,
495 )
496 .build()
497 .unwrap()
498 }
499
500 fn provision_identity(ctx: &AuthsContext) -> KeyAlias {
501 let keychain = MemoryKeychainHandle;
502 let signer = StorageSigner::new(MemoryKeychainHandle);
503 let provider = PrefilledPassphraseProvider::new("Test-passphrase1!");
504 let config = DeveloperSetupConfig::builder(KeyAlias::new_unchecked("test-key"))
505 .with_git_signing_scope(GitSigningScope::Skip)
506 .build();
507 let result = setup_developer(config, ctx, &keychain, &signer, &provider, None).unwrap();
508 result.key_alias
509 }
510
511 #[test]
514 fn resolve_rotation_context_returns_identity_and_prefix() {
515 let ctx = fake_ctx("Test-passphrase1!");
516 let key_alias = provision_identity(&ctx);
517
518 let config = RotationConfig {
519 repo_path: std::path::PathBuf::from("/unused"),
520 identity_key_alias: Some(key_alias.clone()),
521 next_key_alias: None,
522 };
523
524 let (identity, prefix, alias) = resolve_rotation_context(&config, &ctx).unwrap();
525 assert!(identity.controller_did.as_str().starts_with("did:keri:"));
526 assert_eq!(
527 prefix.as_str(),
528 identity
529 .controller_did
530 .as_str()
531 .strip_prefix("did:keri:")
532 .unwrap()
533 );
534 assert_eq!(alias, key_alias);
535 }
536
537 #[test]
538 fn resolve_rotation_context_auto_discovers_alias() {
539 let ctx = fake_ctx("Test-passphrase1!");
540 let _key_alias = provision_identity(&ctx);
541
542 let config = RotationConfig {
543 repo_path: std::path::PathBuf::from("/unused"),
544 identity_key_alias: None,
545 next_key_alias: None,
546 };
547
548 let (_identity, _prefix, alias) = resolve_rotation_context(&config, &ctx).unwrap();
549 assert!(!alias.contains("--next-"));
550 }
551
552 #[test]
553 fn resolve_rotation_context_missing_identity_returns_error() {
554 let ctx = fake_ctx("unused");
555
556 let config = RotationConfig {
557 repo_path: std::path::PathBuf::from("/unused"),
558 identity_key_alias: Some(KeyAlias::new_unchecked("any")),
559 next_key_alias: None,
560 };
561
562 let result = resolve_rotation_context(&config, &ctx);
563 assert!(matches!(
564 result,
565 Err(RotationError::IdentityNotFound { .. })
566 ));
567 }
568
569 #[test]
572 fn retrieve_precommitted_key_succeeds_after_setup() {
573 let ctx = fake_ctx("Test-passphrase1!");
574 let key_alias = provision_identity(&ctx);
575
576 let config = RotationConfig {
577 repo_path: std::path::PathBuf::from("/unused"),
578 identity_key_alias: Some(key_alias.clone()),
579 next_key_alias: None,
580 };
581
582 let (identity, prefix, _) = resolve_rotation_context(&config, &ctx).unwrap();
583 let state = ctx.registry.get_key_state(&prefix).unwrap();
584
585 let (decrypted, old_alias) =
586 retrieve_precommitted_key(&identity.controller_did, &key_alias, &state, &ctx).unwrap();
587
588 assert!(!decrypted.is_empty());
589 assert!(old_alias.contains("--next-"));
590 }
591
592 #[test]
593 fn retrieve_precommitted_key_wrong_did_returns_error() {
594 let ctx = fake_ctx("Test-passphrase1!");
595 let key_alias = provision_identity(&ctx);
596
597 let config = RotationConfig {
598 repo_path: std::path::PathBuf::from("/unused"),
599 identity_key_alias: Some(key_alias.clone()),
600 next_key_alias: None,
601 };
602
603 let (_, prefix, _) = resolve_rotation_context(&config, &ctx).unwrap();
604 let state = ctx.registry.get_key_state(&prefix).unwrap();
605 let wrong_did = IdentityDID::new_unchecked("did:keri:EWrongDid".to_string());
606
607 let result = retrieve_precommitted_key(&wrong_did, &key_alias, &state, &ctx);
608 assert!(matches!(result, Err(RotationError::RotationFailed(_))));
609 }
610
611 #[test]
612 fn retrieve_precommitted_key_missing_key_returns_error() {
613 let ctx = fake_ctx("Test-passphrase1!");
614
615 let did = IdentityDID::new_unchecked("did:keri:Etest".to_string());
616 let state = KeyState {
617 prefix: Prefix::new_unchecked("Etest".to_string()),
618 current_keys: vec![],
619 next_commitment: vec![],
620 sequence: 999,
621 last_event_said: Said::default(),
622 is_abandoned: false,
623 threshold: 1,
624 next_threshold: 1,
625 };
626
627 let result = retrieve_precommitted_key(
628 &did,
629 &KeyAlias::new_unchecked("nonexistent-alias"),
630 &state,
631 &ctx,
632 );
633 assert!(matches!(result, Err(RotationError::KeyNotFound(_))));
634 }
635
636 #[test]
639 fn generate_rotation_keys_produces_valid_event() {
640 let ctx = fake_ctx("Test-passphrase1!");
641 let key_alias = provision_identity(&ctx);
642
643 let config = RotationConfig {
644 repo_path: std::path::PathBuf::from("/unused"),
645 identity_key_alias: Some(key_alias.clone()),
646 next_key_alias: None,
647 };
648
649 let (identity, prefix, _) = resolve_rotation_context(&config, &ctx).unwrap();
650 let state = ctx.registry.get_key_state(&prefix).unwrap();
651 let (decrypted, _) =
652 retrieve_precommitted_key(&identity.controller_did, &key_alias, &state, &ctx).unwrap();
653
654 let (rot, new_next_pkcs8) = generate_rotation_keys(&identity, &state, &decrypted).unwrap();
655
656 assert_eq!(rot.s, KeriSequence::new(state.sequence + 1));
657 assert_eq!(rot.i, prefix);
658 assert!(!rot.d.is_empty());
659 assert!(!rot.x.is_empty());
660 assert!(!new_next_pkcs8.as_ref().is_empty());
661 }
662
663 #[test]
666 fn finalize_rotation_storage_persists_keys() {
667 let ctx = fake_ctx("Test-passphrase1!");
668 let key_alias = provision_identity(&ctx);
669
670 let config = RotationConfig {
671 repo_path: std::path::PathBuf::from("/unused"),
672 identity_key_alias: Some(key_alias.clone()),
673 next_key_alias: None,
674 };
675
676 let (identity, prefix, _) = resolve_rotation_context(&config, &ctx).unwrap();
677 let state = ctx.registry.get_key_state(&prefix).unwrap();
678 let (decrypted, old_next_alias) =
679 retrieve_precommitted_key(&identity.controller_did, &key_alias, &state, &ctx).unwrap();
680 let (rot, new_next_pkcs8) = generate_rotation_keys(&identity, &state, &decrypted).unwrap();
681
682 let rotated_alias = KeyAlias::new_unchecked("rotated-key");
683 let result = finalize_rotation_storage(
684 FinalizeParams {
685 did: &identity.controller_did,
686 prefix: &prefix,
687 next_alias: &rotated_alias,
688 old_next_alias: &old_next_alias,
689 current_pkcs8: &decrypted,
690 new_next_pkcs8: new_next_pkcs8.as_ref(),
691 rot: &rot,
692 state: &state,
693 },
694 &ctx,
695 );
696
697 assert!(
698 result.is_ok(),
699 "finalize_rotation_storage failed: {:?}",
700 result
701 );
702
703 let (loaded_did, _) = ctx
704 .key_storage
705 .load_key(&KeyAlias::new_unchecked("rotated-key"))
706 .unwrap();
707 assert_eq!(loaded_did, identity.controller_did);
708
709 let new_sequence = state.sequence + 1;
710 let next_key_alias = format!("rotated-key--next-{}", new_sequence);
711 let (loaded_next_did, _) = ctx
712 .key_storage
713 .load_key(&KeyAlias::new_unchecked(&next_key_alias))
714 .unwrap();
715 assert_eq!(loaded_next_did, identity.controller_did);
716 }
717
718 #[test]
719 fn finalize_rotation_storage_rejects_mismatched_passphrases() {
720 use std::sync::atomic::{AtomicU32, Ordering};
721
722 struct AlternatingProvider {
723 call_count: AtomicU32,
724 }
725
726 impl PassphraseProvider for AlternatingProvider {
727 fn get_passphrase(
728 &self,
729 _prompt: &str,
730 ) -> Result<zeroize::Zeroizing<String>, auths_core::AgentError> {
731 let n = self.call_count.fetch_add(1, Ordering::SeqCst);
732 if n.is_multiple_of(2) {
733 Ok(zeroize::Zeroizing::new("pass-a".to_string()))
734 } else {
735 Ok(zeroize::Zeroizing::new("pass-b".to_string()))
736 }
737 }
738 }
739
740 let prefix = Prefix::new_unchecked("ETestMismatch".to_string());
741 let did = IdentityDID::new_unchecked("did:keri:ETestMismatch".to_string());
742
743 let state = KeyState {
744 prefix: prefix.clone(),
745 current_keys: vec!["D_key".to_string()],
746 next_commitment: vec!["hash".to_string()],
747 sequence: 0,
748 last_event_said: Said::new_unchecked("EPrior".to_string()),
749 is_abandoned: false,
750 threshold: 1,
751 next_threshold: 1,
752 };
753
754 let rng = SystemRandom::new();
755 let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
756
757 let dummy_rot = RotEvent {
758 v: KERI_VERSION.to_string(),
759 d: Said::new_unchecked("E_dummy".to_string()),
760 i: prefix.clone(),
761 s: KeriSequence::new(1),
762 p: Said::default(),
763 kt: "1".to_string(),
764 k: vec![],
765 nt: "1".to_string(),
766 n: vec![],
767 bt: "0".to_string(),
768 b: vec![],
769 a: vec![],
770 x: String::new(),
771 };
772
773 let ctx =
774 AuthsContext::builder()
775 .registry(
776 Arc::new(FakeRegistryBackend::new()) as Arc<dyn RegistryBackend + Send + Sync>
777 )
778 .key_storage(Arc::new(MemoryKeychainHandle))
779 .clock(Arc::new(SystemClock))
780 .identity_storage(
781 Arc::new(FakeIdentityStorage::new()) as Arc<dyn IdentityStorage + Send + Sync>
782 )
783 .attestation_sink(
784 Arc::new(FakeAttestationSink::new()) as Arc<dyn AttestationSink + Send + Sync>
785 )
786 .attestation_source(Arc::new(FakeAttestationSource::new())
787 as Arc<dyn AttestationSource + Send + Sync>)
788 .passphrase_provider(Arc::new(AlternatingProvider {
789 call_count: AtomicU32::new(0),
790 })
791 as Arc<dyn PassphraseProvider + Send + Sync>)
792 .build()
793 .unwrap();
794
795 let test_alias = KeyAlias::new_unchecked("test-alias");
796 let old_alias = KeyAlias::new_unchecked("old-alias");
797 let result = finalize_rotation_storage(
798 FinalizeParams {
799 did: &did,
800 prefix: &prefix,
801 next_alias: &test_alias,
802 old_next_alias: &old_alias,
803 current_pkcs8: pkcs8.as_ref(),
804 new_next_pkcs8: pkcs8.as_ref(),
805 rot: &dummy_rot,
806 state: &state,
807 },
808 &ctx,
809 );
810
811 assert!(
812 matches!(result, Err(RotationError::RotationFailed(ref msg)) if msg.contains("passphrases do not match")),
813 "Expected passphrase mismatch error, got: {:?}",
814 result
815 );
816 }
817}