use super::VtaClient;
use crate::error::VtaError;
impl VtaClient {
pub async fn fetch_context_secrets(
&self,
context_id: &str,
) -> Result<Vec<affinidi_tdk::secrets_resolver::secrets::Secret>, VtaError> {
let page_size = 100u64;
let mut offset = 0u64;
let mut secrets = Vec::new();
loop {
let resp = self
.list_keys(offset, page_size, Some("active"), Some(context_id))
.await?;
if resp.keys.is_empty() {
break;
}
for key in &resp.keys {
let secret_resp = self.get_key_secret(&key.key_id).await?;
let secret = crate::did_key::secret_from_key_response(&secret_resp)?;
secrets.push(secret);
}
offset += resp.keys.len() as u64;
if offset >= resp.total {
break;
}
}
Ok(secrets)
}
pub async fn fetch_did_secrets_bundle(
&self,
context_id: &str,
) -> Result<crate::did_secrets::DidSecretsBundle, VtaError> {
let ctx = self.get_context(context_id).await?;
let did = ctx.did.ok_or_else(|| {
VtaError::Validation(format!("context '{context_id}' has no DID assigned"))
})?;
let page_size = 100u64;
let mut offset = 0u64;
let mut secrets = Vec::new();
loop {
let resp = self
.list_keys(offset, page_size, Some("active"), Some(context_id))
.await?;
if resp.keys.is_empty() {
break;
}
for key in &resp.keys {
let secret_resp = self.get_key_secret(&key.key_id).await?;
let entry = crate::did_secrets::SecretEntry::from(secret_resp);
match select_secret_kid(&did, &entry.key_id, key.label.as_deref()) {
Some(kid) => secrets.push(crate::did_secrets::SecretEntry {
key_id: kid,
..entry
}),
None => {
tracing::warn!(
context = %context_id,
did = %did,
key_id = %entry.key_id,
label = key.label.as_deref().unwrap_or(""),
"excluding secret from did-secrets bundle: not a verification \
method of the context DID (e.g. an admin did:key minted into \
this context, or a free-text-labelled key). Including it would \
corrupt the DIDComm operating-secret set and break the \
mediator's exact-match recipient lookup."
);
}
}
}
offset += resp.keys.len() as u64;
if offset >= resp.total {
break;
}
}
Ok(crate::did_secrets::DidSecretsBundle { did, secrets })
}
}
fn select_secret_kid(did: &str, record_key_id: &str, label: Option<&str>) -> Option<String> {
let vm_prefix = format!("{did}#");
if record_key_id.starts_with(&vm_prefix) {
return Some(record_key_id.to_string());
}
if let Some(label) = label
&& label.starts_with(&vm_prefix)
&& !label.chars().any(char::is_whitespace)
{
return Some(label.to_string());
}
None
}
#[cfg(test)]
mod tests {
use super::select_secret_kid;
const DID: &str =
"did:webvh:QmQjq4GHRH9fwSXCg4884kxpCMT5EUqHB9XY2U7aXisP8R:webvh.storm.ws:mediator-2";
#[test]
fn keeps_store_vm_id_and_ignores_decorative_label() {
let kid = select_secret_kid(
DID,
&format!("{DID}#key-0"),
Some("did:key:z6Mkr4JCdsEVcQvYKxcyjf39tPmVriDfg3gALvqv4GQHc5BH signing key"),
);
assert_eq!(kid.as_deref(), Some(format!("{DID}#key-0").as_str()));
}
#[test]
fn regression_did_prefixed_label_must_not_clobber_key_id() {
let kid = select_secret_kid(
DID,
&format!("{DID}#key-1"),
Some("did:key:z6Mkr4JCdsEVcQvYKxcyjf39tPmVriDfg3gALvqv4GQHc5BH key-agreement key"),
);
assert_eq!(kid.as_deref(), Some(format!("{DID}#key-1").as_str()));
assert!(!kid.as_deref().unwrap().contains(' '));
}
#[test]
fn excludes_admin_did_key_minted_into_context() {
let admin = "did:key:z6Mkt6eNM38RhFfjSdmXBtT1SRL7sPgPZD1MkXZbwjYBhTLf";
let kid = select_secret_kid(
DID,
&format!("{admin}#z6Mkt6eNM38RhFfjSdmXBtT1SRL7sPgPZD1MkXZbwjYBhTLf"),
Some("admin DID for context mediator-test"),
);
assert_eq!(kid, None);
}
#[test]
fn adopts_label_when_it_is_a_strict_vm_id_and_record_id_is_not() {
let kid = select_secret_kid(DID, "m/26'/2'/3'/4'", Some(&format!("{DID}#key-1")));
assert_eq!(kid.as_deref(), Some(format!("{DID}#key-1").as_str()));
}
#[test]
fn rejects_label_vm_id_with_trailing_free_text() {
let kid = select_secret_kid(DID, "m/26'/2'/3'/4'", Some(&format!("{DID}#key-1 rotated")));
assert_eq!(kid, None);
}
}