use git2::Repository;
use ring::rand::SystemRandom;
use ring::signature::{Ed25519KeyPair, KeyPair};
use std::path::Path;
use auths_crypto::Pkcs8Der;
use crate::error::InitError;
use crate::identity::helpers::load_keypair_from_der_or_seed;
use crate::keri::{
CesrKey, Event, GitKel, KeriSequence, Prefix, RotEvent, Said, Threshold, VersionString,
rotate_keys, serialize_for_signing, validate_kel,
};
use std::sync::Arc;
use crate::storage::layout::StorageLayoutConfig;
use crate::storage::registry::RegistryBackend;
use crate::witness_config::WitnessConfig;
use auths_core::crypto::said::{compute_next_commitment, verify_commitment};
use auths_core::crypto::signer::{decrypt_keypair, encrypt_keypair};
use auths_core::signing::PassphraseProvider;
use auths_core::storage::keychain::{IdentityDID, KeyAlias, KeyRole, KeyStorage};
use auths_keri::compute_said;
pub struct RotationKeyInfo {
pub sequence: u128,
pub new_current_pkcs8: Pkcs8Der,
pub new_next_pkcs8: Pkcs8Der,
}
#[derive(Debug, Clone, Default)]
pub struct RotationShape {
pub add_devices: Vec<auths_crypto::CurveType>,
pub remove_indices: Vec<u32>,
pub new_kt: Option<Threshold>,
pub new_nt: Option<Threshold>,
}
#[allow(clippy::too_many_arguments)]
pub fn rotate_keri_identity(
repo_path: &Path,
current_alias: &KeyAlias,
next_alias: &KeyAlias,
passphrase_provider: &dyn PassphraseProvider,
_config: &StorageLayoutConfig,
keychain: &(dyn KeyStorage + Send + Sync),
witness_config: Option<&WitnessConfig>,
now: chrono::DateTime<chrono::Utc>,
) -> Result<RotationKeyInfo, InitError> {
let repo = Repository::open(repo_path)?;
let (did, _role, _encrypted_current) = keychain.load_key(current_alias)?;
if keychain.is_hardware_backend() {
return Err(InitError::InvalidData(
"Rotation requires a software-backed key; current key is hardware-backed \
(Secure Enclave). Rotate by initializing a new identity."
.into(),
));
}
let prefix = did.as_str().strip_prefix("did:keri:").ok_or_else(|| {
InitError::InvalidData(format!("Invalid DID format, expected 'did:keri:': {}", did))
})?;
let kel = GitKel::new(&repo, prefix);
let events = kel
.get_events()
.map_err(|e| InitError::Keri(e.to_string()))?;
let state = validate_kel(&events).map_err(|e| InitError::Keri(e.to_string()))?;
let derived_next_alias = KeyAlias::new_unchecked(format!(
"{}--next-{}",
current_alias, state.last_establishment_sequence
));
let (did_check, _role, encrypted_next) = keychain.load_key(&derived_next_alias)?;
if did != did_check {
return Err(InitError::InvalidData(format!(
"DID mismatch for pre-committed key '{}': expected {}, found {}",
derived_next_alias, did, did_check
)));
}
let next_pass = passphrase_provider.get_passphrase(&format!(
"Enter passphrase for pre-committed key '{}':",
derived_next_alias
))?;
let decrypted_next_pkcs8 =
Pkcs8Der::new(decrypt_keypair(&encrypted_next, &next_pass)?.to_vec());
let rotation_result = rotate_keys(
&repo,
&Prefix::new_unchecked(prefix.to_string()),
&decrypted_next_pkcs8,
witness_config,
now,
)
.map_err(|e| InitError::Keri(e.to_string()))?;
let new_pass = passphrase_provider.get_passphrase(&format!(
"Create passphrase for new key alias '{}':",
next_alias
))?;
let confirm_pass =
passphrase_provider.get_passphrase(&format!("Confirm passphrase for '{}':", next_alias))?;
if new_pass != confirm_pass {
return Err(InitError::InvalidData(format!(
"Passphrases do not match for alias '{}'",
next_alias
)));
}
let encrypted_new_current = encrypt_keypair(decrypted_next_pkcs8.as_ref(), &new_pass)?;
keychain.store_key(next_alias, &did, KeyRole::Primary, &encrypted_new_current)?;
let encrypted_future =
encrypt_keypair(rotation_result.new_next_keypair_pkcs8.as_ref(), &new_pass)?;
let future_key_alias =
KeyAlias::new_unchecked(format!("{}--next-{}", next_alias, rotation_result.sequence));
keychain.store_key(
&future_key_alias,
&did,
KeyRole::NextRotation,
&encrypted_future,
)?;
let _ = keychain.delete_key(&derived_next_alias);
log::debug!("Cleaned up pre-committed key: {}", derived_next_alias);
Ok(RotationKeyInfo {
sequence: rotation_result.sequence,
new_current_pkcs8: rotation_result.new_current_keypair_pkcs8,
new_next_pkcs8: rotation_result.new_next_keypair_pkcs8,
})
}
#[allow(clippy::too_many_lines)]
pub fn rotate_registry_identity(
backend: Arc<dyn RegistryBackend + Send + Sync>,
current_alias: &KeyAlias,
next_alias: &KeyAlias,
passphrase_provider: &dyn PassphraseProvider,
_config: &StorageLayoutConfig,
keychain: &(dyn KeyStorage + Send + Sync),
witness_config: Option<&WitnessConfig>,
) -> Result<RotationKeyInfo, InitError> {
let rng = SystemRandom::new();
let (did, _role, _encrypted_current) = keychain.load_key(current_alias)?;
if keychain.is_hardware_backend() {
return Err(InitError::InvalidData(
"Rotation requires a software-backed key; current key is hardware-backed \
(Secure Enclave). Rotate by initializing a new identity."
.into(),
));
}
let prefix_str = did.as_str().strip_prefix("did:keri:").ok_or_else(|| {
InitError::InvalidData(format!("Invalid DID format, expected 'did:keri:': {}", did))
})?;
let prefix = Prefix::new_unchecked(prefix_str.to_string());
let state = backend
.get_key_state(&prefix)
.map_err(|e| InitError::Registry(e.to_string()))?;
let derived_next_alias = KeyAlias::new_unchecked(format!(
"{}--next-{}",
current_alias, state.last_establishment_sequence
));
let (did_check, _role, encrypted_next) = keychain.load_key(&derived_next_alias)?;
if did != did_check {
return Err(InitError::InvalidData(format!(
"DID mismatch for pre-committed key '{}': expected {}, found {}",
derived_next_alias, did, did_check
)));
}
let next_pass = passphrase_provider.get_passphrase(&format!(
"Enter passphrase for pre-committed key '{}':",
derived_next_alias
))?;
let decrypted_next_pkcs8 =
Pkcs8Der::new(decrypt_keypair(&encrypted_next, &next_pass)?.to_vec());
if !state.can_rotate() {
return Err(InitError::InvalidData(
"Identity is abandoned (cannot rotate)".into(),
));
}
let next_keypair = load_keypair_from_der_or_seed(decrypted_next_pkcs8.as_ref())?;
#[allow(clippy::expect_used)] let next_verkey = auths_keri::KeriPublicKey::ed25519(next_keypair.public_key().as_ref())
.expect("ring Ed25519 public key is 32 bytes");
if !verify_commitment(&next_verkey, &state.next_commitment[0]) {
return Err(InitError::InvalidData(
"Commitment mismatch: next key does not match previous commitment".into(),
));
}
let new_next_pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng)
.map_err(|e| InitError::Crypto(format!("Key generation failed: {}", e)))?;
let new_next_keypair = Ed25519KeyPair::from_pkcs8(new_next_pkcs8.as_ref())
.map_err(|e| InitError::Crypto(format!("Key generation failed: {}", e)))?;
#[allow(clippy::expect_used)]
let new_current_pub_encoded =
auths_keri::KeriPublicKey::ed25519(next_keypair.public_key().as_ref())
.expect("ring Ed25519 public key is 32 bytes")
.to_qb64()
.expect("cesride verkey encode is infallible");
#[allow(clippy::expect_used)] let new_next_verkey =
auths_keri::KeriPublicKey::ed25519(new_next_keypair.public_key().as_ref())
.expect("ring Ed25519 public key is 32 bytes");
let new_next_commitment = compute_next_commitment(&new_next_verkey);
let bt = match witness_config {
Some(cfg) if cfg.is_enabled() => Threshold::Simple(cfg.threshold as u64),
_ => Threshold::Simple(0),
};
let new_sequence = state.sequence + 1;
let mut rot = RotEvent {
v: VersionString::placeholder(),
d: Said::default(),
i: prefix.clone(),
s: KeriSequence::new(new_sequence),
p: state.last_event_said.clone(),
kt: Threshold::Simple(1),
k: vec![CesrKey::new_unchecked(new_current_pub_encoded)],
nt: Threshold::Simple(1),
n: vec![new_next_commitment],
bt,
br: vec![],
ba: vec![],
c: vec![],
a: vec![],
};
let rot_value = serde_json::to_value(Event::Rot(rot.clone()))
.map_err(|e| InitError::Keri(format!("Serialization failed: {}", e)))?;
rot.d = compute_said(&rot_value)
.map_err(|e| InitError::Keri(format!("SAID computation failed: {}", e)))?;
let canonical = serialize_for_signing(&Event::Rot(rot.clone()))
.map_err(|e| InitError::Keri(e.to_string()))?;
let sig = next_keypair.sign(&canonical);
let attachment = auths_keri::serialize_attachment(&[auths_keri::IndexedSignature {
index: 0,
prior_index: None,
sig: sig.as_ref().to_vec(),
}])
.map_err(|e| InitError::Keri(format!("attachment serialization: {e}")))?;
backend
.append_signed_event(&prefix, &Event::Rot(rot), &attachment)
.map_err(|e| InitError::Registry(e.to_string()))?;
store_rotated_keys(
keychain,
passphrase_provider,
&did,
next_alias,
&derived_next_alias,
new_sequence,
decrypted_next_pkcs8.as_ref(),
new_next_pkcs8.as_ref(),
)?;
Ok(RotationKeyInfo {
sequence: new_sequence,
new_current_pkcs8: decrypted_next_pkcs8,
new_next_pkcs8: Pkcs8Der::new(new_next_pkcs8.as_ref()),
})
}
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
pub fn rotate_registry_identity_multi(
backend: Arc<dyn RegistryBackend + Send + Sync>,
current_alias: &KeyAlias,
next_alias: &KeyAlias,
passphrase_provider: &dyn PassphraseProvider,
_config: &StorageLayoutConfig,
keychain: &(dyn KeyStorage + Send + Sync),
witness_config: Option<&WitnessConfig>,
shape: RotationShape,
) -> Result<RotationKeyInfo, InitError> {
let first_cur = KeyAlias::new_unchecked(format!("{}--{}", current_alias, 0));
let (did, _role, _encrypted) = keychain
.load_key(&first_cur)
.or_else(|_| keychain.load_key(current_alias))?;
if keychain.is_hardware_backend() {
return Err(InitError::InvalidData(
"Rotation requires software-backed keys; current slot is hardware-backed.".to_string(),
));
}
let prefix_str = did.as_str().strip_prefix("did:keri:").ok_or_else(|| {
InitError::InvalidData(format!("Invalid DID format, expected 'did:keri:': {}", did))
})?;
let prefix = Prefix::new_unchecked(prefix_str.to_string());
let state = backend
.get_key_state(&prefix)
.map_err(|e| InitError::Registry(e.to_string()))?;
if !state.can_rotate() {
return Err(InitError::InvalidData(
"Identity is abandoned (cannot rotate)".to_string(),
));
}
let prior_key_count = state.current_keys.len();
let mut remove: Vec<usize> = shape.remove_indices.iter().map(|&i| i as usize).collect();
remove.sort_unstable();
remove.dedup();
if let Some(&bad) = remove.iter().find(|&&i| i >= prior_key_count) {
return Err(InitError::InvalidData(format!(
"remove index {bad} out of range (prior controller count {prior_key_count})"
)));
}
let surviving_count = prior_key_count - remove.len();
if surviving_count == 0 {
return Err(InitError::InvalidData(
"rotation must retain at least one prior controller to authorise it".to_string(),
));
}
let new_key_count = surviving_count + shape.add_devices.len();
let new_kt = shape.new_kt.unwrap_or_else(|| state.threshold.clone());
let new_nt = shape.new_nt.unwrap_or_else(|| state.next_threshold.clone());
crate::keri::inception::validate_threshold_for_key_count(&new_kt, new_key_count)
.map_err(|e| InitError::InvalidData(e.to_string()))?;
crate::keri::inception::validate_threshold_for_key_count(&new_nt, new_key_count)
.map_err(|e| InitError::InvalidData(e.to_string()))?;
let mut new_current_pkcs8s: Vec<Pkcs8Der> = Vec::with_capacity(new_key_count);
let mut new_current_pubs: Vec<auths_keri::KeriPublicKey> = Vec::with_capacity(new_key_count);
let mut prior_next_aliases: Vec<KeyAlias> = Vec::with_capacity(prior_key_count);
let mut prior_indices: Vec<u32> = Vec::with_capacity(surviving_count);
let next_pass = passphrase_provider.get_passphrase(&format!(
"Enter passphrase for pre-committed keys under alias '{}':",
current_alias
))?;
for idx in 0..prior_key_count {
let alias = KeyAlias::new_unchecked(format!(
"{}--next-{}-{}",
current_alias, state.sequence, idx
));
if remove.contains(&idx) {
prior_next_aliases.push(alias);
continue;
}
let (did_check, _role, encrypted) = keychain.load_key(&alias)?;
if did_check != did {
return Err(InitError::InvalidData(format!(
"DID mismatch for pre-committed key '{}'",
alias
)));
}
let pkcs8 = Pkcs8Der::new(decrypt_keypair(&encrypted, &next_pass)?.to_vec());
let keypair = load_keypair_from_der_or_seed(pkcs8.as_ref())?;
#[allow(clippy::expect_used)] let next_verkey = auths_keri::KeriPublicKey::ed25519(keypair.public_key().as_ref())
.expect("ring Ed25519 public key is 32 bytes");
if !verify_commitment(&next_verkey, &state.next_commitment[idx]) {
return Err(InitError::InvalidData(format!(
"Commitment mismatch at slot {idx}: next key does not match previous commitment"
)));
}
new_current_pubs.push(next_verkey);
new_current_pkcs8s.push(pkcs8);
prior_indices.push(idx as u32);
prior_next_aliases.push(alias);
}
if !shape.add_devices.is_empty() {
let added = crate::keri::inception::generate_keypairs_for_init(&shape.add_devices)
.map_err(|e| InitError::Crypto(e.to_string()))?;
for kp in &added {
new_current_pubs.push(kp.verkey());
new_current_pkcs8s.push(kp.pkcs8.clone());
}
}
let new_next_curves: Vec<auths_crypto::CurveType> = (0..new_key_count)
.map(|_| auths_crypto::CurveType::P256)
.collect();
let new_next_kps = crate::keri::inception::generate_keypairs_for_init(&new_next_curves)
.map_err(|e| InitError::Crypto(e.to_string()))?;
let k: Vec<CesrKey> = new_current_pubs
.iter()
.map(|vk| {
vk.to_qb64()
.map(CesrKey::new_unchecked)
.map_err(|e| InitError::InvalidData(e.to_string()))
})
.collect::<Result<_, _>>()?;
let n: Vec<Said> = new_next_kps
.iter()
.map(|kp| compute_next_commitment(&kp.verkey()))
.collect();
let bt = match witness_config {
Some(cfg) if cfg.is_enabled() => Threshold::Simple(cfg.threshold as u64),
_ => Threshold::Simple(0),
};
let new_sequence = state.sequence + 1;
let mut rot = RotEvent {
v: VersionString::placeholder(),
d: Said::default(),
i: prefix.clone(),
s: KeriSequence::new(new_sequence),
p: state.last_event_said.clone(),
kt: new_kt,
k,
nt: new_nt,
n,
bt,
br: vec![],
ba: vec![],
c: vec![],
a: vec![],
};
let rot_value = serde_json::to_value(Event::Rot(rot.clone()))
.map_err(|e| InitError::Keri(format!("Serialization failed: {}", e)))?;
rot.d = compute_said(&rot_value)
.map_err(|e| InitError::Keri(format!("SAID computation failed: {}", e)))?;
let canonical = serialize_for_signing(&Event::Rot(rot.clone()))
.map_err(|e| InitError::Keri(e.to_string()))?;
let signer_keypair = load_keypair_from_der_or_seed(new_current_pkcs8s[0].as_ref())?;
let sig = signer_keypair.sign(&canonical);
let attachment = auths_keri::serialize_attachment(&[auths_keri::IndexedSignature {
index: 0,
prior_index: prior_indices.first().copied(),
sig: sig.as_ref().to_vec(),
}])
.map_err(|e| InitError::Keri(format!("attachment serialization: {e}")))?;
backend
.append_signed_event(&prefix, &Event::Rot(rot), &attachment)
.map_err(|e| InitError::Registry(e.to_string()))?;
let new_pass = passphrase_provider.get_passphrase(&format!(
"Create passphrase for new key alias '{}':",
next_alias
))?;
let confirm_pass =
passphrase_provider.get_passphrase(&format!("Confirm passphrase for '{}':", next_alias))?;
if new_pass != confirm_pass {
return Err(InitError::InvalidData(format!(
"Passphrases do not match for alias '{}'",
next_alias
)));
}
for (idx, cur_pkcs8) in new_current_pkcs8s.iter().enumerate() {
let slot_alias = KeyAlias::new_unchecked(format!("{}--{}", next_alias, idx));
let encrypted = encrypt_keypair(cur_pkcs8.as_ref(), &new_pass)?;
keychain.store_key(&slot_alias, &did, KeyRole::Primary, &encrypted)?;
}
for (idx, nxt_kp) in new_next_kps.iter().enumerate() {
let slot_alias =
KeyAlias::new_unchecked(format!("{}--next-{}-{}", next_alias, new_sequence, idx));
let encrypted = encrypt_keypair(nxt_kp.pkcs8.as_ref(), &new_pass)?;
keychain.store_key(&slot_alias, &did, KeyRole::NextRotation, &encrypted)?;
}
for alias in &prior_next_aliases {
let _ = keychain.delete_key(alias);
}
let new_next_pkcs8_bytes = new_next_kps[0].pkcs8.as_ref().to_vec();
Ok(RotationKeyInfo {
sequence: new_sequence,
new_current_pkcs8: new_current_pkcs8s
.into_iter()
.next()
.ok_or_else(|| InitError::Crypto("empty current keyset after rotation".to_string()))?,
new_next_pkcs8: Pkcs8Der::new(new_next_pkcs8_bytes),
})
}
#[allow(clippy::too_many_arguments)]
fn store_rotated_keys(
keychain: &(dyn KeyStorage + Send + Sync),
passphrase_provider: &dyn PassphraseProvider,
did: &IdentityDID,
next_alias: &KeyAlias,
old_next_alias: &KeyAlias,
new_sequence: u128,
current_pkcs8: &[u8],
new_next_pkcs8: &[u8],
) -> Result<(), InitError> {
let new_pass = passphrase_provider.get_passphrase(&format!(
"Create passphrase for new key alias '{}':",
next_alias
))?;
let confirm_pass =
passphrase_provider.get_passphrase(&format!("Confirm passphrase for '{}':", next_alias))?;
if new_pass != confirm_pass {
return Err(InitError::InvalidData(format!(
"Passphrases do not match for alias '{}'",
next_alias
)));
}
let encrypted_new_current = encrypt_keypair(current_pkcs8, &new_pass)?;
keychain.store_key(next_alias, did, KeyRole::Primary, &encrypted_new_current)?;
let encrypted_future = encrypt_keypair(new_next_pkcs8, &new_pass)?;
let future_key_alias =
KeyAlias::new_unchecked(format!("{}--next-{}", next_alias, new_sequence));
keychain.store_key(
&future_key_alias,
did,
KeyRole::NextRotation,
&encrypted_future,
)?;
let _ = keychain.delete_key(old_next_alias);
Ok(())
}