use serde::{Deserialize, Serialize};
use crate::keys::KeyType;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DidSecretsBundle {
pub did: String,
pub secrets: Vec<SecretEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecretEntry {
pub key_id: String,
pub key_type: KeyType,
pub private_key_multibase: String,
}
#[cfg(feature = "client")]
impl From<crate::client::GetKeySecretResponse> for SecretEntry {
fn from(resp: crate::client::GetKeySecretResponse) -> Self {
Self {
key_id: resp.key_id,
key_type: resp.key_type,
private_key_multibase: resp.private_key_multibase,
}
}
}
pub 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::*;
#[test]
fn test_serde_json_roundtrip() {
let bundle = DidSecretsBundle {
did: "did:webvh:abc123:example.com".to_string(),
secrets: vec![
SecretEntry {
key_id: "did:webvh:abc123:example.com#key-0".to_string(),
key_type: KeyType::Ed25519,
private_key_multibase: "z6Mk...signing".to_string(),
},
SecretEntry {
key_id: "did:webvh:abc123:example.com#key-1".to_string(),
key_type: KeyType::X25519,
private_key_multibase: "z6Mk...ka".to_string(),
},
],
};
let json = serde_json::to_string(&bundle).unwrap();
let decoded: DidSecretsBundle = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.did, bundle.did);
assert_eq!(decoded.secrets.len(), 2);
assert_eq!(decoded.secrets[0].key_id, bundle.secrets[0].key_id);
assert_eq!(decoded.secrets[0].key_type, KeyType::Ed25519);
assert_eq!(
decoded.secrets[0].private_key_multibase,
bundle.secrets[0].private_key_multibase
);
assert_eq!(decoded.secrets[1].key_type, KeyType::X25519);
}
#[test]
fn test_serde_json_empty_secrets() {
let bundle = DidSecretsBundle {
did: "did:example:123".to_string(),
secrets: vec![],
};
let json = serde_json::to_string(&bundle).unwrap();
let decoded: DidSecretsBundle = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.did, "did:example:123");
assert!(decoded.secrets.is_empty());
}
const KID_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(
KID_DID,
&format!("{KID_DID}#key-0"),
Some("did:key:z6Mkr4JCdsEVcQvYKxcyjf39tPmVriDfg3gALvqv4GQHc5BH signing key"),
);
assert_eq!(kid.as_deref(), Some(format!("{KID_DID}#key-0").as_str()));
}
#[test]
fn regression_did_prefixed_label_must_not_clobber_key_id() {
let kid = select_secret_kid(
KID_DID,
&format!("{KID_DID}#key-1"),
Some("did:key:z6Mkr4JCdsEVcQvYKxcyjf39tPmVriDfg3gALvqv4GQHc5BH key-agreement key"),
);
assert_eq!(kid.as_deref(), Some(format!("{KID_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(
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(KID_DID, "m/26'/2'/3'/4'", Some(&format!("{KID_DID}#key-1")));
assert_eq!(kid.as_deref(), Some(format!("{KID_DID}#key-1").as_str()));
}
#[test]
fn rejects_label_vm_id_with_trailing_free_text() {
let kid = select_secret_kid(
KID_DID,
"m/26'/2'/3'/4'",
Some(&format!("{KID_DID}#key-1 rotated")),
);
assert_eq!(kid, None);
}
}