use crate::{
KeyPurpose,
bip32::Bip32Extension,
config::{
KeyBackend, KeyTypes,
account::PersonaId,
secured_config::{KeyInfoConfig, KeySourceMaterial},
},
errors::OpenVTCError,
vrc::Vrcs,
};
use affinidi_tdk::{
TDK,
did_common::{document::DocumentExt, verification_method::VerificationMethod},
didcomm::Message,
messaging::{ATM, profiles::ATMProfile},
secrets_resolver::{SecretsResolver, secrets::Secret},
};
use chrono::{DateTime, Utc};
use secrecy::ExposeSecret;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::{
collections::{HashMap, HashSet},
fmt::Display,
sync::Arc,
time::SystemTime,
};
use tracing::{debug, warn};
use uuid::Uuid;
#[derive(Clone, Debug, Hash, Serialize, Deserialize, PartialEq, Eq)]
pub enum RelationshipState {
RequestSent,
RequestAccepted,
RequestRejected,
Established,
None,
}
impl Display for RelationshipState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let state_str = match self {
RelationshipState::RequestSent => "Request Sent",
RelationshipState::RequestAccepted => "Request Accepted",
RelationshipState::RequestRejected => "Request Rejected",
RelationshipState::Established => "Established",
RelationshipState::None => "None",
};
write!(f, "{}", state_str)
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(from = "RelationshipsShadow", into = "RelationshipsShadow")]
pub struct Relationships {
pub relationships: HashMap<Arc<String>, Relationship>,
pub path_pointer: u32,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct Relationship {
pub task_id: Arc<String>,
pub our_did: Arc<String>,
pub remote_did: Arc<String>,
pub remote_p_did: Arc<String>,
pub created: DateTime<Utc>,
pub state: RelationshipState,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub our_persona: Option<PersonaId>,
#[serde(default, skip_serializing_if = "is_false")]
pub needs_reestablishment: bool,
}
fn is_false(b: &bool) -> bool {
!*b
}
pub fn relationship_signing_vm_id(r_did: &str) -> String {
format!("{r_did}#key-1")
}
pub fn relationship_encryption_vm_id(r_did: &str) -> String {
format!("{r_did}#key-2")
}
pub async fn relationship_signing_secret(
tdk: &affinidi_tdk::TDK,
r_did: &str,
) -> Option<affinidi_tdk::secrets_resolver::secrets::Secret> {
use affinidi_tdk::secrets_resolver::SecretsResolver;
tdk.get_shared_state()
.secrets_resolver()
.get_secret(&relationship_signing_vm_id(r_did))
.await
}
#[derive(Debug, Default)]
pub struct KeyInfoRepairReport {
pub repaired: usize,
pub unrecoverable: Vec<Arc<String>>,
}
impl KeyInfoRepairReport {
pub fn changed(&self) -> bool {
self.repaired > 0 || !self.unrecoverable.is_empty()
}
}
fn find_key_source(
vm: &VerificationMethod,
target_bytes: &[u8],
kp: KeyPurpose,
key_info: &HashMap<String, KeyInfoConfig>,
key_backend: &KeyBackend,
path_pointer: u32,
) -> Option<(Option<String>, KeySourceMaterial, DateTime<Utc>)> {
if let Some(mb) = vm
.property_set
.get("publicKeyMultibase")
.and_then(|v| v.as_str())
&& let Some(ki) = key_info.get(mb)
{
return Some((Some(mb.to_string()), ki.path.clone(), ki.create_time));
}
for (id, ki) in key_info.iter() {
let bytes = match &ki.path {
KeySourceMaterial::Derived { path } => match key_backend {
KeyBackend::Bip32 { root, .. } => root
.get_secret_from_path(path, kp)
.ok()
.map(|s| s.get_public_bytes().to_vec()),
_ => None,
},
KeySourceMaterial::Imported { seed } => {
Secret::from_multibase(seed.expose_secret(), None)
.ok()
.map(|s| s.get_public_bytes().to_vec())
}
KeySourceMaterial::VtaManaged { .. } => None,
};
if bytes.as_deref() == Some(target_bytes) {
return Some((Some(id.clone()), ki.path.clone(), ki.create_time));
}
}
if let KeyBackend::Bip32 { root, .. } = key_backend {
for i in 0..path_pointer {
let path = format!("m/3'/1'/1'/{i}'");
if let Ok(s) = root.get_secret_from_path(&path, kp)
&& s.get_public_bytes() == target_bytes
{
return Some((None, KeySourceMaterial::Derived { path }, Utc::now()));
}
}
}
None
}
impl From<RelationshipsShadow> for Relationships {
fn from(value: RelationshipsShadow) -> Self {
let mut relationships: HashMap<Arc<String>, Relationship> = HashMap::new();
for relationship in value.relationships {
let key = relationship.remote_p_did.clone();
relationships.insert(key, relationship);
}
Relationships {
relationships,
path_pointer: value.path_pointer,
}
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(default)]
pub(crate) struct RelationshipsShadow {
pub(crate) relationships: Vec<Relationship>,
pub(crate) path_pointer: u32,
}
impl From<Relationships> for RelationshipsShadow {
fn from(value: Relationships) -> Self {
let relationships = value
.relationships
.into_values()
.collect::<Vec<Relationship>>();
RelationshipsShadow {
relationships,
path_pointer: value.path_pointer,
}
}
}
impl Relationships {
pub async fn generate_profiles(
&self,
tdk: &TDK,
our_p_dids: &HashSet<String>,
mediator: &str,
key_backend: &KeyBackend,
key_info: &HashMap<String, KeyInfoConfig>,
vta_client: Option<&vta_sdk::client::VtaClient>,
) -> Result<HashMap<Arc<String>, Arc<ATMProfile>>, OpenVTCError> {
let atm = tdk
.atm
.clone()
.ok_or_else(|| OpenVTCError::Config("TDK ATM service not initialized".to_string()))?;
let mut profiles: HashMap<Arc<String>, Arc<ATMProfile>> = HashMap::new();
debug!(
"generating {} relationship profiles",
self.relationships.len()
);
let mut owned_vta_client: Option<vta_sdk::client::VtaClient> = None;
let vta_client: Option<&vta_sdk::client::VtaClient> = match vta_client {
Some(client) => Some(client),
None => {
if matches!(key_backend, KeyBackend::Vta { .. }) {
owned_vta_client =
Some(super::config::build_runtime_vta_client(key_backend).await?);
owned_vta_client.as_ref()
} else {
None
}
}
};
let profiles_result: Result<(), OpenVTCError> = async {
let r_did_entries: Vec<Arc<String>> = self
.relationships
.values()
.filter_map(|rel| {
if matches!(
rel.state,
RelationshipState::Established
| RelationshipState::RequestSent
| RelationshipState::RequestAccepted
) && !our_p_dids.contains(rel.our_did.as_str())
&& !rel.needs_reestablishment
{
Some(rel.our_did.clone())
} else {
None
}
})
.collect();
struct PendingVtaFetch {
key_id: String,
secret_id: String,
purpose: KeyPurpose,
}
let mut all_secrets: Vec<Secret> = Vec::new();
let mut vta_fetches: Vec<PendingVtaFetch> = Vec::new();
for our_did in &r_did_entries {
let has_key_agreement = key_info.iter().any(|(k, v)| {
k.starts_with(our_did.as_str())
&& matches!(v.purpose, KeyTypes::RelationshipEncryption)
});
if !has_key_agreement {
warn!(
r_did = %our_did,
"relationship R-DID has no usable key-agreement key; skipping its \
messaging profile — this relationship must be re-established"
);
continue;
}
let profile =
ATMProfile::new(&atm, None, our_did.to_string(), Some(mediator.to_string()))
.await?;
profiles.insert(our_did.clone(), atm.profile_add(&profile, false).await?);
for (k, v) in key_info.iter() {
if !k.starts_with(our_did.as_str()) {
continue;
}
let kp = match v.purpose {
KeyTypes::RelationshipVerification => KeyPurpose::Signing,
KeyTypes::RelationshipEncryption => KeyPurpose::Encryption,
_ => continue,
};
match &v.path {
KeySourceMaterial::Derived { path } => {
if let KeyBackend::Bip32 { root, .. } = key_backend
&& let Ok(mut s) = root.get_secret_from_path(path, kp)
{
s.id = k.clone();
all_secrets.push(s);
}
}
KeySourceMaterial::Imported { seed } => {
if let Ok(mut s) = Secret::from_multibase(seed.expose_secret(), None) {
s.id = k.clone();
all_secrets.push(s);
}
}
KeySourceMaterial::VtaManaged { key_id } => {
vta_fetches.push(PendingVtaFetch {
key_id: key_id.clone(),
secret_id: k.clone(),
purpose: kp,
});
}
}
}
}
if let Some(client) = vta_client
&& !vta_fetches.is_empty()
{
debug!("fetching {} VTA secrets concurrently", vta_fetches.len());
let mut handles = Vec::with_capacity(vta_fetches.len());
for fetch in &vta_fetches {
let client = client.clone();
let key_id = fetch.key_id.clone();
handles.push(tokio::spawn(
async move { client.get_key_secret(&key_id).await },
));
}
for (fetch, handle) in vta_fetches.iter().zip(handles) {
match handle.await {
Ok(Ok(resp)) => {
if let Ok(mut s) =
crate::config::keys::secret_from_vta_response(&resp, fetch.purpose)
{
s.id = fetch.secret_id.clone();
all_secrets.push(s);
}
}
Ok(Err(e)) => {
warn!(key_id = %fetch.key_id, "VTA get_key_secret failed: {e}");
}
Err(e) => {
warn!(key_id = %fetch.key_id, "VTA fetch task panicked: {e}");
}
}
}
}
if !all_secrets.is_empty() {
tdk.get_shared_state()
.secrets_resolver()
.insert_vec(&all_secrets)
.await;
}
Ok(())
}
.await;
if let Some(client) = &owned_vta_client {
client.shutdown().await;
}
profiles_result?;
Ok(profiles)
}
pub async fn repair_key_info_ids(
&mut self,
tdk: &TDK,
our_p_dids: &HashSet<String>,
key_info: &mut HashMap<String, KeyInfoConfig>,
key_backend: &KeyBackend,
) -> KeyInfoRepairReport {
let mut report = KeyInfoRepairReport::default();
let targets: Vec<(Arc<String>, Arc<String>)> = self
.relationships
.values()
.filter(|rel| {
matches!(
rel.state,
RelationshipState::Established
| RelationshipState::RequestSent
| RelationshipState::RequestAccepted
) && !our_p_dids.contains(rel.our_did.as_str())
})
.map(|rel| (rel.remote_p_did.clone(), rel.our_did.clone()))
.collect();
let path_pointer = self.path_pointer;
let mut to_flag: Vec<Arc<String>> = Vec::new();
for (remote_p_did, r_did) in targets {
if key_info.keys().any(|k| k.starts_with(r_did.as_str())) {
continue;
}
let doc = match tdk.did_resolver().resolve(r_did.as_str()).await {
Ok(resp) => resp.doc,
Err(e) => {
warn!(r_did = %r_did, error = %e,
"repair: could not resolve R-DID; relationship must be re-established");
report.unrecoverable.push(r_did);
to_flag.push(remote_p_did);
continue;
}
};
let mut rekeys: Vec<(Option<String>, String, KeyInfoConfig)> = Vec::new();
let mut recovered = true;
for vm in &doc.verification_method {
let Ok(target_bytes) = vm.get_public_key_bytes() else {
continue; };
let (kp, purpose) = if doc.contains_key_agreement(vm.id.as_str()) {
(KeyPurpose::Encryption, KeyTypes::RelationshipEncryption)
} else {
(KeyPurpose::Signing, KeyTypes::RelationshipVerification)
};
match find_key_source(vm, &target_bytes, kp, key_info, key_backend, path_pointer) {
Some((old_id, source, created)) => rekeys.push((
old_id,
vm.id.as_str().to_string(),
KeyInfoConfig {
path: source,
create_time: created,
purpose,
},
)),
None => {
recovered = false;
break;
}
}
}
if !recovered || rekeys.is_empty() {
warn!(r_did = %r_did,
"repair: relationship keys could not be recovered; must be re-established");
report.unrecoverable.push(r_did);
to_flag.push(remote_p_did);
continue;
}
for (old_id, new_id, new_ki) in rekeys {
if let Some(old) = old_id
&& old != new_id
{
key_info.remove(&old);
}
key_info.insert(new_id, new_ki);
report.repaired += 1;
}
debug!(r_did = %r_did, "repair: re-keyed relationship key_info to canonical ids");
}
for pdid in to_flag {
if let Some(rel) = self.relationships.get_mut(&pdid) {
rel.needs_reestablishment = true;
}
}
report
}
pub fn remove_by_task_id(
&mut self,
id: &Arc<String>,
vrcs_issued: &mut Vrcs,
vrcs_received: &mut Vrcs,
) -> Option<Relationship> {
let key = self
.relationships
.iter()
.find(|(_, r)| r.task_id == *id)
.map(|(k, _)| k.clone());
if let Some(key) = key {
debug!("relationship removed: task_id={}", id);
self.remove(&key, vrcs_issued, vrcs_received)
} else {
None
}
}
pub fn remove(
&mut self,
key: &Arc<String>,
vrcs_issued: &mut Vrcs,
vrcs_received: &mut Vrcs,
) -> Option<Relationship> {
vrcs_issued.remove_relationship(key);
vrcs_received.remove_relationship(key);
let removed = self.relationships.remove(key);
if removed.is_some() {
debug!("relationship removed: remote_did={}", key);
}
removed
}
pub fn get(&self, p_did: &Arc<String>) -> Option<&Relationship> {
self.relationships.get(p_did)
}
pub fn get_mut(&mut self, p_did: &Arc<String>) -> Option<&mut Relationship> {
self.relationships.get_mut(p_did)
}
pub fn find_by_task_id(&self, task_id: &Arc<String>) -> Option<&Relationship> {
self.relationships.values().find(|r| &r.task_id == task_id)
}
pub fn find_key_by_task_id(&self, task_id: &Arc<String>) -> Option<Arc<String>> {
self.relationships
.iter()
.find(|(_, r)| &r.task_id == task_id)
.map(|(k, _)| k.clone())
}
pub fn awaiting(
&self,
thid: &Arc<String>,
state: RelationshipState,
from: &str,
) -> Option<Arc<String>> {
if thid.is_empty() {
return None;
}
self.relationships
.iter()
.find(|(_, r)| &r.task_id == thid)
.filter(|(_, r)| r.state == state && r.remote_p_did.as_str() == from)
.map(|(k, _)| k.clone())
}
pub fn find_by_remote_did(&self, did: &Arc<String>) -> Option<&Relationship> {
self.relationships
.values()
.find(|r| r.remote_did == *did || r.remote_p_did == *did)
}
pub fn find_key_by_remote_did(&self, did: &Arc<String>) -> Option<Arc<String>> {
self.relationships
.iter()
.find(|(_, r)| r.remote_did == *did || r.remote_p_did == *did)
.map(|(k, _)| k.clone())
}
pub fn get_established_relationships(&self) -> Vec<&Relationship> {
self.relationships
.values()
.filter(|r| r.state == RelationshipState::Established)
.collect()
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct RelationshipRequestBody {
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
pub did: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, rename = "didProof", skip_serializing_if = "Option::is_none")]
pub did_proof: Option<serde_json::Value>,
#[serde(
default,
rename = "personaProof",
skip_serializing_if = "Option::is_none"
)]
pub persona_proof: Option<serde_json::Value>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct RelationshipRejectBody {
pub reason: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct RelationshipAcceptBody {
pub did: String,
#[serde(default, rename = "didProof", skip_serializing_if = "Option::is_none")]
pub did_proof: Option<serde_json::Value>,
#[serde(
default,
rename = "personaProof",
skip_serializing_if = "Option::is_none"
)]
pub persona_proof: Option<serde_json::Value>,
}
pub const RELATIONSHIP_DID_BINDING_TYPE: &str =
"https://linuxfoundation.org/openvtc/1.0/relationship-did-binding";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BindingRole {
Request,
Accept,
}
impl BindingRole {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
BindingRole::Request => "request",
BindingRole::Accept => "accept",
}
}
}
fn did_binding(
did: &str,
persona: &str,
peer: &str,
thid: &str,
role: BindingRole,
) -> serde_json::Value {
json!({
"type": RELATIONSHIP_DID_BINDING_TYPE,
"role": role.as_str(),
"did": did,
"persona": persona,
"peer": peer,
"thid": thid,
})
}
pub async fn sign_did_binding(
did: &str,
persona: &str,
peer: &str,
thid: &str,
role: BindingRole,
signer: &Secret,
) -> Result<serde_json::Value, OpenVTCError> {
use affinidi_data_integrity::{DataIntegrityProof, SignOptions};
let statement = did_binding(did, persona, peer, thid, role);
let proof = DataIntegrityProof::sign(
&statement,
signer,
SignOptions::new().with_proof_purpose(crate::proof_check::Purpose::Authentication.as_str()),
)
.await
.map_err(|e| OpenVTCError::Config(format!("could not sign the relationship DID proof: {e}")))?;
serde_json::to_value(proof)
.map_err(|e| OpenVTCError::Config(format!("relationship DID proof: {e}")))
}
pub async fn did_binding_proofs(
did: &str,
persona: &str,
peer: &str,
thid: &str,
role: BindingRole,
did_signer: &Secret,
persona_signer: &Secret,
) -> Result<(serde_json::Value, Option<serde_json::Value>), OpenVTCError> {
let did_proof = sign_did_binding(did, persona, peer, thid, role, did_signer).await?;
let persona_proof = if did == persona {
None
} else {
Some(sign_did_binding(did, persona, peer, thid, role, persona_signer).await?)
};
Ok((did_proof, persona_proof))
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum DidBindingError {
#[error("its proofs were not checked")]
NotChecked,
#[error("its proofs' check did not finish (timed out or failed)")]
CheckUnfinished,
#[error("it carries no proof of control of the relationship DID")]
MissingDidProof,
#[error("it carries no proof from the persona naming the relationship DID")]
MissingPersonaProof,
#[error("the relationship DID's proof: {0}")]
DidProof(crate::proof_check::ProofError),
#[error("the persona's proof: {0}")]
PersonaProof(crate::proof_check::ProofError),
}
#[allow(clippy::too_many_arguments)]
pub async fn verify_did_binding(
did: &str,
did_proof: Option<&serde_json::Value>,
persona: &str,
persona_proof: Option<&serde_json::Value>,
peer: &str,
thid: &str,
role: BindingRole,
resolver: &affinidi_did_resolver_cache_sdk::DIDCacheClient,
) -> Result<(), DidBindingError> {
use crate::proof_check::{Purpose, verify_signed};
let statement = did_binding(did, persona, peer, thid, role);
let with = |proof: &serde_json::Value| {
let mut signed = statement.clone();
signed["proof"] = proof.clone();
signed
};
let did_proof = did_proof.ok_or(DidBindingError::MissingDidProof)?;
verify_signed(&with(did_proof), did, resolver, &[Purpose::Authentication])
.await
.map_err(DidBindingError::DidProof)?;
if did != persona {
let persona_proof = persona_proof.ok_or(DidBindingError::MissingPersonaProof)?;
verify_signed(
&with(persona_proof),
persona,
resolver,
&[Purpose::Authentication],
)
.await
.map_err(DidBindingError::PersonaProof)?;
}
Ok(())
}
pub async fn create_send_message_rejected(
atm: &ATM,
from_profile: &Arc<ATMProfile>,
to: &str,
mediator_did: &str,
reason: Option<&str>,
thid: &str,
) -> Result<(), OpenVTCError> {
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map_err(|e| OpenVTCError::Config(format!("System clock error: {e}")))?
.as_secs();
let msg = Message::build(
Uuid::new_v4().to_string(),
"https://linuxfoundation.org/openvtc/1.0/relationship-request-reject".to_string(),
json!(RelationshipRejectBody {
reason: reason.map(|r| r.to_string())
}),
)
.from(from_profile.inner.did.to_string())
.to(to.to_string())
.thid(thid.to_string())
.created_time(now)
.expires_time(now + 60 * 60 * 48) .finalize();
crate::pack_and_send(
atm,
from_profile,
&msg,
&from_profile.inner.did,
to,
mediator_did,
)
.await?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use ed25519_dalek_bip32::ExtendedSigningKey;
use secrecy::SecretString;
fn make_relationship(
task_id: &str,
our_did: &str,
remote_did: &str,
remote_p_did: &str,
state: RelationshipState,
) -> Relationship {
Relationship {
task_id: Arc::new(task_id.to_string()),
our_did: Arc::new(our_did.to_string()),
remote_did: Arc::new(remote_did.to_string()),
remote_p_did: Arc::new(remote_p_did.to_string()),
created: Utc::now(),
state,
our_persona: None,
needs_reestablishment: false,
}
}
#[test]
fn a_reply_answers_only_the_request_it_threads_on() {
let mut rels = Relationships::default();
rels.relationships.insert(
Arc::new("did:peer:bob".into()),
make_relationship(
"req-1",
"did:peer:me",
"did:peer:bob",
"did:peer:bob",
RelationshipState::RequestSent,
),
);
rels.relationships.insert(
Arc::new("did:peer:carol".into()),
make_relationship(
"req-2",
"did:peer:me",
"did:peer:carol-r",
"did:peer:carol",
RelationshipState::Established,
),
);
let t = |s: &str| Arc::new(s.to_string());
assert_eq!(
rels.awaiting(&t("req-1"), RelationshipState::RequestSent, "did:peer:bob"),
Some(t("did:peer:bob"))
);
assert_eq!(
rels.awaiting(
&t("req-1"),
RelationshipState::RequestSent,
"did:peer:mallory"
),
None
);
assert_eq!(
rels.awaiting(&t(""), RelationshipState::RequestSent, "did:peer:bob"),
None
);
assert_eq!(
rels.awaiting(
&t("unknown"),
RelationshipState::RequestSent,
"did:peer:bob"
),
None
);
assert_eq!(
rels.awaiting(
&t("req-2"),
RelationshipState::RequestSent,
"did:peer:carol"
),
None
);
assert_eq!(
rels.awaiting(
&t("req-1"),
RelationshipState::RequestAccepted,
"did:peer:bob"
),
None
);
}
fn did_key_signer(seed: u8) -> (String, Secret) {
let mut secret = Secret::generate_ed25519(None, Some(&[seed; 32]));
let public = secret.get_public_keymultibase().unwrap();
let did = format!("did:key:{public}");
secret.id = format!("{did}#{public}");
(did, secret)
}
fn rdid_signer() -> (String, Secret) {
use affinidi_tdk::dids::{DID, KeyType, PeerKeyRole};
let (did, secrets) = DID::generate_did_peer(
vec![
(PeerKeyRole::Verification, KeyType::Ed25519),
(PeerKeyRole::Encryption, KeyType::X25519),
],
None,
)
.expect("mint a did:peer");
let signer = secrets
.into_iter()
.find(|s| s.id == relationship_signing_vm_id(&did))
.expect("the verification key");
(did, signer)
}
async fn resolver() -> affinidi_did_resolver_cache_sdk::DIDCacheClient {
affinidi_did_resolver_cache_sdk::DIDCacheClient::new(
affinidi_did_resolver_cache_sdk::config::DIDCacheConfigBuilder::default().build(),
)
.await
.expect("resolver")
}
const PEER: &str = "did:key:z6MkPeer";
const THID: &str = "5f1c2a4e-0000-4000-8000-000000000001";
#[tokio::test]
async fn a_relationship_did_must_be_proven_for_this_handshake() {
let resolver = resolver().await;
let (persona, persona_key) = did_key_signer(0x21);
let (rdid, rdid_key) = rdid_signer();
let (did_proof, persona_proof) = did_binding_proofs(
&rdid,
&persona,
PEER,
THID,
BindingRole::Request,
&rdid_key,
&persona_key,
)
.await
.unwrap();
assert!(persona_proof.is_some(), "an R-DID is named by its persona");
let verify_as = |did: &str,
did_proof: Option<serde_json::Value>,
persona_proof: Option<serde_json::Value>,
thid: &str,
role: BindingRole| {
let (did, thid, persona, resolver) = (
did.to_string(),
thid.to_string(),
persona.clone(),
resolver.clone(),
);
async move {
verify_did_binding(
&did,
did_proof.as_ref(),
&persona,
persona_proof.as_ref(),
PEER,
&thid,
role,
&resolver,
)
.await
}
};
let verify = |did: &str,
did_proof: Option<serde_json::Value>,
persona_proof: Option<serde_json::Value>,
thid: &str| {
verify_as(did, did_proof, persona_proof, thid, BindingRole::Request)
};
assert_eq!(
verify(&rdid, Some(did_proof.clone()), persona_proof.clone(), THID).await,
Ok(())
);
assert_eq!(
verify(&rdid, None, persona_proof.clone(), THID).await,
Err(DidBindingError::MissingDidProof)
);
assert_eq!(
verify(&rdid, Some(did_proof.clone()), None, THID).await,
Err(DidBindingError::MissingPersonaProof)
);
assert!(matches!(
verify(
&rdid,
Some(did_proof.clone()),
persona_proof.clone(),
"another-thread"
)
.await,
Err(DidBindingError::DidProof(
crate::proof_check::ProofError::Invalid(0)
))
));
assert!(matches!(
verify_as(
&rdid,
Some(did_proof.clone()),
persona_proof.clone(),
THID,
BindingRole::Accept
)
.await,
Err(DidBindingError::DidProof(
crate::proof_check::ProofError::Invalid(0)
))
));
let (victim, _) = did_key_signer(0x33);
let (forged, _) = did_binding_proofs(
&victim,
&persona,
PEER,
THID,
BindingRole::Request,
&rdid_key,
&persona_key,
)
.await
.unwrap();
assert!(matches!(
verify(&victim, Some(forged), None, THID).await,
Err(DidBindingError::DidProof(
crate::proof_check::ProofError::ForeignVerificationMethod(0)
))
));
let (own, none) = did_binding_proofs(
&persona,
&persona,
PEER,
THID,
BindingRole::Request,
&persona_key,
&persona_key,
)
.await
.unwrap();
assert!(none.is_none());
assert_eq!(verify(&persona, Some(own), None, THID).await, Ok(()));
}
async fn empty_tdk() -> TDK {
use affinidi_tdk::common::config::TDKConfig;
TDK::new(
TDKConfig::builder()
.with_load_environment(false)
.build()
.expect("TDK config builds"),
None,
)
.await
.expect("TDK builds")
}
fn mint_rdid(root: &ExtendedSigningKey, v_path: &str, e_path: &str) -> String {
use affinidi_tdk::dids::{DID, PeerKeyRole};
let mut v = root
.get_secret_from_path(v_path, KeyPurpose::Signing)
.expect("v key");
let mut e = root
.get_secret_from_path(e_path, KeyPurpose::Encryption)
.expect("e key");
let mut keys = vec![
(PeerKeyRole::Verification, &mut v),
(PeerKeyRole::Encryption, &mut e),
];
DID::generate_did_peer_from_secrets(&mut keys, Some("did:web:mediator.example".to_string()))
.expect("did:peer")
}
fn broken_entry(path: &str, purpose: KeyTypes) -> KeyInfoConfig {
KeyInfoConfig {
path: KeySourceMaterial::Derived {
path: path.to_string(),
},
create_time: Utc::now(),
purpose,
}
}
#[tokio::test]
async fn repair_rekeys_bip32_rdid_keyinfo_to_canonical_ids() {
let root = ExtendedSigningKey::from_seed(&[42u8; 32]).expect("root");
let key_backend = KeyBackend::Bip32 {
root: ExtendedSigningKey::from_seed(&[42u8; 32]).expect("root"),
seed: SecretString::new("seed".into()),
};
let (v_path, e_path) = ("m/3'/1'/1'/0'", "m/3'/1'/1'/1'");
let r_did = mint_rdid(&root, v_path, e_path);
let mut key_info = HashMap::new();
key_info.insert(
"random-v-id".to_string(),
broken_entry(v_path, KeyTypes::RelationshipVerification),
);
key_info.insert(
"x25519".to_string(),
broken_entry(e_path, KeyTypes::RelationshipEncryption),
);
let mut rels = Relationships {
path_pointer: 2,
..Default::default()
};
rels.relationships.insert(
Arc::new("did:remote:p".to_string()),
make_relationship(
"task-1",
&r_did,
"did:remote:r",
"did:remote:p",
RelationshipState::Established,
),
);
let tdk = empty_tdk().await;
let report = rels
.repair_key_info_ids(&tdk, &HashSet::new(), &mut key_info, &key_backend)
.await;
assert_eq!(report.repaired, 2, "both keys re-keyed");
assert!(report.unrecoverable.is_empty());
let v_id = format!("{r_did}#key-1");
let e_id = format!("{r_did}#key-2");
assert!(key_info.contains_key(&v_id), "verification → {v_id}");
assert!(key_info.contains_key(&e_id), "key-agreement → {e_id}");
assert!(matches!(
key_info[&e_id].purpose,
KeyTypes::RelationshipEncryption
));
assert!(matches!(
key_info[&v_id].purpose,
KeyTypes::RelationshipVerification
));
assert!(!key_info.contains_key("random-v-id"));
assert!(!key_info.contains_key("x25519"));
assert!(
!rels.relationships[&Arc::new("did:remote:p".to_string())].needs_reestablishment,
"recovered relationship is not flagged"
);
let again = rels
.repair_key_info_ids(&tdk, &HashSet::new(), &mut key_info, &key_backend)
.await;
assert_eq!(again.repaired, 0);
assert!(!again.changed());
}
#[tokio::test]
async fn repair_recovers_encryption_key_lost_to_x25519_collision() {
let root = ExtendedSigningKey::from_seed(&[7u8; 32]).expect("root");
let key_backend = KeyBackend::Bip32 {
root: ExtendedSigningKey::from_seed(&[7u8; 32]).expect("root"),
seed: SecretString::new("seed".into()),
};
let a = mint_rdid(&root, "m/3'/1'/1'/0'", "m/3'/1'/1'/1'");
let b = mint_rdid(&root, "m/3'/1'/1'/2'", "m/3'/1'/1'/3'");
let mut key_info = HashMap::new();
key_info.insert(
"rand-a".to_string(),
broken_entry("m/3'/1'/1'/0'", KeyTypes::RelationshipVerification),
);
key_info.insert(
"rand-b".to_string(),
broken_entry("m/3'/1'/1'/2'", KeyTypes::RelationshipVerification),
);
key_info.insert(
"x25519".to_string(),
broken_entry("m/3'/1'/1'/3'", KeyTypes::RelationshipEncryption),
);
let mut rels = Relationships {
path_pointer: 4,
..Default::default()
};
for (i, did) in [&a, &b].into_iter().enumerate() {
rels.relationships.insert(
Arc::new(format!("did:remote:p{i}")),
make_relationship(
&format!("task-{i}"),
did,
"did:remote:r",
&format!("did:remote:p{i}"),
RelationshipState::Established,
),
);
}
let tdk = empty_tdk().await;
let report = rels
.repair_key_info_ids(&tdk, &HashSet::new(), &mut key_info, &key_backend)
.await;
assert!(
report.unrecoverable.is_empty(),
"rescan recovers the collided key: {:?}",
report.unrecoverable
);
for did in [&a, &b] {
assert!(
key_info.contains_key(&format!("{did}#key-1")),
"{did} verification key present"
);
assert!(
key_info.contains_key(&format!("{did}#key-2")),
"{did} key-agreement key present (rescan-recovered if collided)"
);
}
}
#[tokio::test]
async fn repair_flags_unrecoverable_relationship() {
let foreign = ExtendedSigningKey::from_seed(&[99u8; 32]).expect("foreign root");
let r_did = mint_rdid(&foreign, "m/3'/1'/1'/0'", "m/3'/1'/1'/1'");
let key_backend = KeyBackend::Bip32 {
root: ExtendedSigningKey::from_seed(&[1u8; 32]).expect("local root"),
seed: SecretString::new("seed".into()),
};
let mut key_info = HashMap::new();
let mut rels = Relationships {
path_pointer: 4,
..Default::default()
};
rels.relationships.insert(
Arc::new("did:remote:p".to_string()),
make_relationship(
"task-1",
&r_did,
"did:remote:r",
"did:remote:p",
RelationshipState::Established,
),
);
let tdk = empty_tdk().await;
let report = rels
.repair_key_info_ids(&tdk, &HashSet::new(), &mut key_info, &key_backend)
.await;
assert_eq!(report.repaired, 0);
assert_eq!(report.unrecoverable.len(), 1);
assert_eq!(report.unrecoverable[0].as_str(), r_did.as_str());
assert!(key_info.is_empty());
assert!(
rels.relationships[&Arc::new("did:remote:p".to_string())].needs_reestablishment,
"unrecoverable relationship is flagged for re-establishment"
);
}
#[test]
fn test_relationships_default_empty() {
let rels = Relationships::default();
assert!(
rels.relationships.is_empty(),
"Default Relationships should have no entries"
);
assert_eq!(rels.path_pointer, 0);
}
#[test]
fn test_add_and_find_relationship() {
let mut rels = Relationships::default();
let r = make_relationship(
"task-1",
"did:our:1",
"did:remote:1",
"did:remote-p:1",
RelationshipState::Established,
);
let key = r.remote_p_did.clone();
rels.relationships.insert(key.clone(), r);
let found = rels.get(&key);
assert!(found.is_some(), "Should find relationship by remote P-DID");
let found_task = rels.find_by_task_id(&Arc::new("task-1".to_string()));
assert!(found_task.is_some(), "Should find relationship by task ID");
let found_remote = rels.find_by_remote_did(&Arc::new("did:remote:1".to_string()));
assert!(
found_remote.is_some(),
"Should find relationship by remote DID"
);
}
#[test]
fn test_get_established_relationships() {
let mut rels = Relationships::default();
let r1 = make_relationship(
"t1",
"did:our:1",
"did:r:1",
"did:rp:1",
RelationshipState::Established,
);
let r2 = make_relationship(
"t2",
"did:our:2",
"did:r:2",
"did:rp:2",
RelationshipState::RequestSent,
);
rels.relationships.insert(r1.remote_p_did.clone(), r1);
rels.relationships.insert(r2.remote_p_did.clone(), r2);
let established = rels.get_established_relationships();
assert_eq!(
established.len(),
1,
"Only one relationship should be established"
);
}
#[test]
fn test_remove_relationship() {
let mut rels = Relationships::default();
let mut vrcs_issued = crate::vrc::Vrcs::default();
let mut vrcs_received = crate::vrc::Vrcs::default();
let r = make_relationship(
"t1",
"did:our:1",
"did:r:1",
"did:rp:1",
RelationshipState::Established,
);
let key = r.remote_p_did.clone();
rels.relationships.insert(key.clone(), r);
let removed = rels.remove(&key, &mut vrcs_issued, &mut vrcs_received);
assert!(removed.is_some(), "Should return the removed relationship");
assert!(
rels.relationships.is_empty(),
"Relationships should be empty after removal"
);
}
#[test]
fn test_relationship_state_display() {
assert_eq!(RelationshipState::RequestSent.to_string(), "Request Sent");
assert_eq!(
RelationshipState::RequestAccepted.to_string(),
"Request Accepted"
);
assert_eq!(
RelationshipState::RequestRejected.to_string(),
"Request Rejected"
);
assert_eq!(RelationshipState::Established.to_string(), "Established");
assert_eq!(RelationshipState::None.to_string(), "None");
}
#[test]
fn test_relationships_shadow_roundtrip() {
let mut rels = Relationships {
path_pointer: 42,
..Default::default()
};
let r = make_relationship(
"t1",
"did:our:1",
"did:r:1",
"did:rp:1",
RelationshipState::Established,
);
rels.relationships.insert(r.remote_p_did.clone(), r);
let shadow: RelationshipsShadow = rels.into();
assert_eq!(shadow.path_pointer, 42);
assert_eq!(shadow.relationships.len(), 1);
let restored: Relationships = shadow.into();
assert_eq!(restored.path_pointer, 42);
assert_eq!(restored.relationships.len(), 1);
}
#[test]
fn relationships_ondisk_byte_identical_roundtrip() {
let fixture = r#"{
"relationships": [
{
"task_id": "task-abc",
"our_did": "did:webvh:example:us",
"remote_did": "did:webvh:example:them-rdid",
"remote_p_did": "did:webvh:example:them",
"created": "2024-01-02T03:04:05Z",
"state": "Established"
}
],
"path_pointer": 7
}"#;
let rels: Relationships = serde_json::from_str(fixture).expect("fixture deserializes");
assert_eq!(rels.path_pointer, 7);
assert_eq!(rels.relationships.len(), 1);
let reserialized = serde_json::to_string_pretty(&rels).expect("re-serializes");
let fixture_value: serde_json::Value =
serde_json::from_str(fixture).expect("fixture is valid json");
let reserialized_value: serde_json::Value =
serde_json::from_str(&reserialized).expect("output is valid json");
assert_eq!(
reserialized_value, fixture_value,
"re-serialized relationships must match the pre-R20 on-disk shape"
);
}
}