use affinidi_tdk::secrets_resolver::secrets::Secret;
use chrono::Utc;
use didwebvh_rs::multibase_type::Multibase;
use vti_common::slip10::{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_paths, path_at, peek_path_counter, peek_paths};
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> {
derive_webvh_keys_block(keys_ks, seed_store, base_path, count, None).await
}
pub(in crate::operations::did_webvh) async fn derive_webvh_keys_block(
keys_ks: &KeyspaceHandle,
seed_store: &dyn SeedStore,
base_path: &str,
count: u32,
expected_start: Option<u32>,
) -> Result<Vec<DerivedWebvhKey>, UpdateDidWebvhError> {
if count == 0 {
return Ok(vec![]);
}
let paths = allocate_paths(keys_ks, base_path, count, expected_start)
.await
.map_err(|e| UpdateDidWebvhError::Persistence(format!("allocate_paths: {e}")))?;
derive_webvh_keys_at(keys_ks, seed_store, &paths).await
}
pub(in crate::operations::did_webvh) async fn peek_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 paths = peek_paths(keys_ks, base_path, count)
.await
.map_err(|e| UpdateDidWebvhError::Persistence(format!("peek_paths: {e}")))?;
derive_webvh_keys_at(keys_ks, seed_store, &paths).await
}
async fn derive_webvh_keys_at(
keys_ks: &KeyspaceHandle,
seed_store: &dyn SeedStore,
paths: &[String],
) -> Result<Vec<DerivedWebvhKey>, UpdateDidWebvhError> {
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(paths.len());
for path in paths {
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.clone(),
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,
seed_store: &dyn SeedStore,
base_path: &str,
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);
}
if let Some(handle) =
recover_signing_key_by_hash(keys_ks, seed_store, base_path, scid, &hash).await?
{
return Ok(handle);
}
}
Err(UpdateDidWebvhError::Library(format!(
"no active update key for DID with SCID {scid} found in keys keyspace, and none of \
its committed keys could be re-derived from the seed"
)))
}
async fn recover_signing_key_by_hash(
keys_ks: &KeyspaceHandle,
seed_store: &dyn SeedStore,
base_path: &str,
scid: &str,
target_hash: &str,
) -> Result<Option<WebvhKeyHandle>, UpdateDidWebvhError> {
let counter = peek_path_counter(keys_ks, base_path).await.map_err(|e| {
UpdateDidWebvhError::Persistence(format!("peek_path_counter for key recovery: {e}"))
})?;
for i in 0..=counter {
let path = path_at(base_path, i);
let derived =
derive_webvh_keys_at(keys_ks, seed_store, std::slice::from_ref(&path)).await?;
let Some(key) = derived.into_iter().find(|k| k.hash == target_hash) else {
continue;
};
tracing::info!(
scid,
target_hash,
path = %key.derivation_path,
"recovered a signing key by re-deriving it from the seed (handle cache miss)"
);
return Ok(Some(WebvhKeyHandle {
scid: scid.to_string(),
version_id: format!("recovered-{i}"),
hash: key.hash,
public_key: key.public_key,
derivation_path: key.derivation_path,
seed_id: Some(key.seed_id),
role: WebvhKeyRole::UpdateKey,
label: "re-derived from seed (handle cache miss)".to_string(),
created_at: Utc::now(),
}));
}
Ok(None)
}
pub(in crate::operations::did_webvh) async fn load_pre_rotation_signing_key(
keys_ks: &KeyspaceHandle,
seed_store: &dyn SeedStore,
base_path: &str,
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);
}
if let Some(handle) =
recover_signing_key_by_hash(keys_ks, seed_store, base_path, scid, hash).await?
{
return Ok(handle);
}
}
Err(UpdateDidWebvhError::Library(format!(
"no pre-rotation key found for any committed hash, and none could be re-derived from \
the seed: {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)
}