use affinidi_tdk::secrets_resolver::secrets::Secret;
use chrono::Utc;
use didwebvh_rs::multibase_type::Multibase;
use ed25519_dalek_bip32::{DerivationPath, ExtendedSigningKey};
use super::errors::UpdateDidWebvhError;
use super::legacy::{legacy_lookup_by_public_key, legacy_lookup_pre_rotation_by_hash};
use super::options::DerivedWebvhKey;
use crate::keys::paths::allocate_path;
use crate::keys::seed_store::SeedStore;
use crate::keys::seeds::{get_active_seed_id, load_seed_bytes};
use crate::operations::did_webvh::webvh_keys::{self, WebvhKeyHandle, WebvhKeyRole};
use crate::store::KeyspaceHandle;
pub(in crate::operations::did_webvh) async fn derive_webvh_keys(
keys_ks: &KeyspaceHandle,
seed_store: &dyn SeedStore,
base_path: &str,
count: u32,
) -> Result<Vec<DerivedWebvhKey>, UpdateDidWebvhError> {
if count == 0 {
return Ok(vec![]);
}
let seed_id = get_active_seed_id(keys_ks).await.map_err(|e| {
UpdateDidWebvhError::Persistence(format!("could not load active seed id: {e}"))
})?;
let seed = load_seed_bytes(keys_ks, seed_store, Some(seed_id))
.await
.map_err(|e| UpdateDidWebvhError::Persistence(format!("could not load seed: {e}")))?;
let root = ExtendedSigningKey::from_seed(&seed)
.map_err(|e| UpdateDidWebvhError::Persistence(format!("BIP-32 root derivation: {e}")))?;
let mut derived = Vec::with_capacity(count as usize);
for _ in 0..count {
let path = allocate_path(keys_ks, base_path)
.await
.map_err(|e| UpdateDidWebvhError::Persistence(format!("allocate_path: {e}")))?;
let parsed: DerivationPath = path.parse().map_err(|e| {
UpdateDidWebvhError::Persistence(format!("parse derivation path `{path}`: {e}"))
})?;
let key = root
.derive(&parsed)
.map_err(|e| UpdateDidWebvhError::Persistence(format!("derive at `{path}`: {e}")))?;
let secret = Secret::generate_ed25519(None, Some(key.signing_key.as_bytes()));
let public_key = secret
.get_public_keymultibase()
.map_err(|e| UpdateDidWebvhError::Persistence(format!("public key encoding: {e}")))?;
let hash = secret
.get_public_keymultibase_hash()
.map_err(|e| UpdateDidWebvhError::Persistence(format!("public key hash: {e}")))?;
derived.push(DerivedWebvhKey {
public_key,
hash,
derivation_path: path,
seed_id,
});
}
Ok(derived)
}
#[allow(clippy::too_many_arguments)]
pub(in crate::operations::did_webvh) async fn install_derived_webvh_keys(
keys_ks: &KeyspaceHandle,
scid: &str,
version_id: &str,
role: WebvhKeyRole,
derived: &[DerivedWebvhKey],
label_prefix: &str,
) -> Result<(), UpdateDidWebvhError> {
let now = Utc::now();
for (i, key) in derived.iter().enumerate() {
let handle = WebvhKeyHandle {
scid: scid.to_string(),
version_id: version_id.to_string(),
hash: key.hash.clone(),
public_key: key.public_key.clone(),
derivation_path: key.derivation_path.clone(),
seed_id: Some(key.seed_id),
role,
label: format!("{label_prefix} #{i}"),
created_at: now,
};
webvh_keys::install(keys_ks, &handle)
.await
.map_err(|e| UpdateDidWebvhError::Persistence(format!("install webvh handle: {e}")))?;
}
Ok(())
}
fn hash_public_key_multibase(pubkey_multibase: &str) -> Result<String, UpdateDidWebvhError> {
Secret::base58_hash_string(pubkey_multibase).map_err(|e| {
UpdateDidWebvhError::Library(format!(
"could not hash public key `{pubkey_multibase}`: {e}"
))
})
}
pub(in crate::operations::did_webvh) async fn load_active_update_key(
keys_ks: &KeyspaceHandle,
scid: &str,
update_keys: &[Multibase],
) -> Result<WebvhKeyHandle, UpdateDidWebvhError> {
if update_keys.is_empty() {
return Err(UpdateDidWebvhError::Library(
"log entry has no update_keys — DID is deactivated or malformed".into(),
));
}
for pubkey_mb in update_keys {
let pubkey_str = pubkey_mb.as_ref();
let hash = hash_public_key_multibase(pubkey_str)?;
match webvh_keys::find_handle_by_hash(keys_ks, scid, &hash).await {
Ok(Some(handle)) => {
if matches!(handle.role, WebvhKeyRole::UpdateKey)
|| matches!(handle.role, WebvhKeyRole::PreRotation)
{
return Ok(handle);
}
return Ok(handle);
}
Ok(None) => {}
Err(e) => {
return Err(UpdateDidWebvhError::Persistence(format!(
"webvh_keys lookup failed: {e}"
)));
}
}
if let Some(handle) = legacy_lookup_by_public_key(keys_ks, scid, pubkey_str, &hash).await? {
return Ok(handle);
}
}
Err(UpdateDidWebvhError::Library(format!(
"no active update key for DID with SCID {scid} found in keys keyspace — \
operator may need to restore key material from backup"
)))
}
pub(in crate::operations::did_webvh) async fn load_pre_rotation_signing_key(
keys_ks: &KeyspaceHandle,
scid: &str,
committed_hashes: &[String],
) -> Result<WebvhKeyHandle, UpdateDidWebvhError> {
if committed_hashes.is_empty() {
return Err(UpdateDidWebvhError::Library(
"previous entry has empty next_key_hashes — pre-rotation reveal impossible".into(),
));
}
tracing::debug!(
scid,
hashes = ?committed_hashes,
"load_pre_rotation_signing_key: searching for committed pre-rotation candidate"
);
for hash in committed_hashes {
match webvh_keys::find_handle_by_hash(keys_ks, scid, hash).await {
Ok(Some(handle)) => {
tracing::debug!(
scid,
hash,
role = ?handle.role,
public_key = %handle.public_key,
"load_pre_rotation_signing_key: fast-path hit"
);
return Ok(handle);
}
Ok(None) => {}
Err(e) => {
return Err(UpdateDidWebvhError::Persistence(format!(
"webvh_keys lookup by hash: {e}"
)));
}
}
if let Some(handle) = legacy_lookup_pre_rotation_by_hash(keys_ks, scid, hash).await? {
tracing::debug!(
scid,
hash,
public_key = %handle.public_key,
"load_pre_rotation_signing_key: legacy fallback hit"
);
return Ok(handle);
}
}
Err(UpdateDidWebvhError::Library(format!(
"no pre-rotation key found for any committed hash: {committed_hashes:?}"
)))
}
pub(in crate::operations::did_webvh) async fn derive_secret_for_handle(
keys_ks: &KeyspaceHandle,
seed_store: &dyn SeedStore,
handle: &WebvhKeyHandle,
) -> Result<Secret, UpdateDidWebvhError> {
let seed = load_seed_bytes(keys_ks, seed_store, handle.seed_id)
.await
.map_err(|e| UpdateDidWebvhError::Persistence(format!("load seed: {e}")))?;
let root = ExtendedSigningKey::from_seed(&seed)
.map_err(|e| UpdateDidWebvhError::Persistence(format!("BIP-32 root: {e}")))?;
let path: DerivationPath = handle.derivation_path.parse().map_err(|e| {
UpdateDidWebvhError::Persistence(format!("parse path `{}`: {e}", handle.derivation_path))
})?;
let derived = root.derive(&path).map_err(|e| {
UpdateDidWebvhError::Persistence(format!("derive at `{}`: {e}", handle.derivation_path))
})?;
let mut secret = Secret::generate_ed25519(None, Some(derived.signing_key.as_bytes()));
secret.id = format!("did:key:{mb}#{mb}", mb = handle.public_key);
Ok(secret)
}