#![cfg(all(feature = "webvh", feature = "didcomm"))]
use std::sync::Arc;
use std::time::Duration;
use affinidi_messaging_core::{MessageTransport, Protocol};
use affinidi_messaging_delivery::{Delivery, InMemoryOutboxStore, MessagingService, OutboxStore};
use affinidi_messaging_didcomm::Message;
use affinidi_tdk::common::TDKSharedState;
use affinidi_tdk::common::config::TDKConfig;
use affinidi_tdk::messaging::config::ATMConfig;
use affinidi_tdk::messaging::profiles::ATMProfile;
use affinidi_tdk::messaging::{ATM, DidCommTransport};
use affinidi_tdk::secrets_resolver::SecretsResolver;
use affinidi_tdk::secrets_resolver::secrets::Secret;
use futures_util::StreamExt;
use serde_json::{Value as JsonValue, json};
use tokio_util::sync::CancellationToken;
use tracing::warn;
use vti_common::telemetry::{SharedTelemetrySink, TelemetryEvent, TelemetryKind};
use crate::messaging::handshake::{
HandshakeError, HandshakeOptions, HandshakeStage, ProverFailure, ResolvedMediator,
resolve_mediator,
};
const TRUST_PING_TYPE: &str = "https://didcomm.org/trust-ping/2.0/ping";
const TRUST_PING_RESPONSE_TYPE: &str = "https://didcomm.org/trust-ping/2.0/ping-response";
pub struct TransientHandshakeContext {
pub vta_did: String,
pub secrets: Vec<Secret>,
pub tdk_config: Option<TDKConfig>,
}
pub async fn run_transient_handshake(
ctx: TransientHandshakeContext,
resolver: &affinidi_did_resolver_cache_sdk::DIDCacheClient,
telemetry: &SharedTelemetrySink,
mediator_did: &str,
opts: HandshakeOptions,
) -> Result<ResolvedMediator, HandshakeError> {
let resolved = match resolve_mediator(resolver, mediator_did).await {
Ok(r) => r,
Err(cause) => {
emit_failed(telemetry, mediator_did, HandshakeStage::Resolve, &cause).await;
return Err(HandshakeError::Failed {
stage: HandshakeStage::Resolve,
cause,
});
}
};
if opts.force {
let _ = telemetry
.record(
TelemetryEvent::new(TelemetryKind::MediatorHandshakeBypassed)
.with_mediator(mediator_did)
.with_field("endpoint", JsonValue::from(resolved.endpoint.clone())),
)
.await;
return Ok(resolved);
}
match transient_prove(&ctx, mediator_did, opts.timeout).await {
Ok(()) => {
let _ = telemetry
.record(
TelemetryEvent::new(TelemetryKind::MediatorHandshakeOk)
.with_mediator(mediator_did)
.with_field("endpoint", JsonValue::from(resolved.endpoint.clone())),
)
.await;
Ok(resolved)
}
Err(failure) => {
emit_failed(telemetry, mediator_did, failure.stage, &failure.cause).await;
Err(HandshakeError::Failed {
stage: failure.stage,
cause: failure.cause,
})
}
}
}
async fn transient_prove(
ctx: &TransientHandshakeContext,
mediator_did: &str,
timeout: Duration,
) -> Result<(), ProverFailure> {
let connect_fail = |cause: String| ProverFailure {
stage: HandshakeStage::Connect,
cause,
};
let tdk_config = match ctx.tdk_config.clone() {
Some(c) => c,
None => TDKConfig::builder()
.build()
.map_err(|e| connect_fail(format!("build TDK config: {e}")))?,
};
let tdk = TDKSharedState::new(tdk_config)
.await
.map_err(|e| connect_fail(format!("create TDK shared state: {e}")))?;
for secret in &ctx.secrets {
tdk.secrets_resolver().insert(secret.clone()).await;
}
let atm = Arc::new(
ATM::new(
ATMConfig::builder()
.build()
.map_err(|e| connect_fail(format!("build ATM config: {e}")))?,
Arc::new(tdk),
)
.await
.map_err(|e| connect_fail(format!("create ATM: {e}")))?,
);
let outcome = prove_on(&atm, ctx, mediator_did, timeout).await;
teardown_transient(&atm, mediator_did).await;
outcome
}
async fn prove_on(
atm: &Arc<ATM>,
ctx: &TransientHandshakeContext,
mediator_did: &str,
timeout: Duration,
) -> Result<(), ProverFailure> {
let connect_fail = |cause: String| ProverFailure {
stage: HandshakeStage::Connect,
cause,
};
let profile = ATMProfile::new(
atm,
Some(mediator_did.to_string()),
ctx.vta_did.clone(),
Some(mediator_did.to_string()),
)
.await
.map_err(|e| connect_fail(format!("create transient profile: {e}")))?;
let profile = atm
.profile_add(&profile, false)
.await
.map_err(|e| connect_fail(format!("register transient profile: {e}")))?;
match tokio::time::timeout(timeout, atm.profile_enable_websocket(&profile)).await {
Ok(Ok(())) => {}
Ok(Err(e)) => return Err(connect_fail(format!("enable transient websocket: {e}"))),
Err(_) => {
return Err(connect_fail(
"timeout enabling transient mediator websocket".to_string(),
));
}
}
let transport: Arc<dyn MessageTransport> = Arc::new(
DidCommTransport::new((**atm).clone(), profile.clone())
.await
.map_err(|e| connect_fail(format!("bind transient transport: {e}")))?,
);
let outbox: Arc<dyn OutboxStore> = Arc::new(InMemoryOutboxStore::new());
let service = Arc::new(MessagingService::new(transport, outbox));
let answerer_shutdown = CancellationToken::new();
spawn_ping_answerer(
service.clone(),
atm.clone(),
ctx.vta_did.clone(),
answerer_shutdown.clone(),
);
let result = ping_self(&service, atm, &ctx.vta_did, timeout).await;
answerer_shutdown.cancel();
tokio::time::sleep(Duration::from_millis(50)).await;
result
}
async fn teardown_transient(atm: &Arc<ATM>, mediator_did: &str) {
if let Err(e) = atm.profile_remove(mediator_did).await {
warn!(mediator = %mediator_did, error = %e, "could not remove the transient profile");
}
atm.graceful_shutdown().await;
}
async fn ping_self(
service: &Arc<MessagingService>,
atm: &ATM,
vta_did: &str,
timeout: Duration,
) -> Result<(), ProverFailure> {
let ping_fail = |cause: String| ProverFailure {
stage: HandshakeStage::TrustPing,
cause,
};
let msg_id = uuid::Uuid::new_v4().to_string();
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let ping = Message::build(
msg_id.clone(),
TRUST_PING_TYPE.to_string(),
json!({ "response_requested": true }),
)
.from(vta_did.to_string())
.to(vta_did.to_string())
.created_time(now)
.expires_time(now + timeout.as_secs())
.finalize();
let (packed, _) = atm
.pack_encrypted(&ping, vta_did, Some(vta_did), Some(vta_did))
.await
.map_err(|e| ping_fail(format!("pack trust-ping: {e}")))?;
let received = service
.request(vta_did, packed.into_bytes(), &msg_id, timeout)
.await
.map_err(|e| ping_fail(format!("trust-ping round-trip failed: {e}")))?;
let response: Message = serde_json::from_slice(&received.payload)
.map_err(|e| ping_fail(format!("parse pong: {e}")))?;
if response.typ != TRUST_PING_RESPONSE_TYPE {
return Err(ping_fail(format!(
"unexpected reply to trust-ping: {}",
response.typ
)));
}
Ok(())
}
fn spawn_ping_answerer(
service: Arc<MessagingService>,
atm: Arc<ATM>,
vta_did: String,
shutdown: CancellationToken,
) {
tokio::spawn(async move {
let mut stream = service.subscribe();
loop {
tokio::select! {
maybe = stream.next() => {
let Some(inbound) = maybe else { break };
if inbound.message.protocol != Protocol::DIDComm {
continue;
}
let Ok(msg) = serde_json::from_slice::<Message>(&inbound.message.payload) else {
continue;
};
if msg.typ != TRUST_PING_TYPE {
continue;
}
let to = inbound
.message
.sender
.clone()
.filter(|_| inbound.message.verified)
.or_else(|| msg.from.clone());
let Some(to) = to else { continue };
let pong = Message::build(
uuid::Uuid::new_v4().to_string(),
TRUST_PING_RESPONSE_TYPE.to_string(),
JsonValue::Null,
)
.from(vta_did.clone())
.to(to.clone())
.thid(msg.id.clone())
.finalize();
if let Ok((packed, _)) = atm
.pack_encrypted(&pong, &to, Some(&vta_did), Some(&vta_did))
.await
&& let Err(e) = service
.send(&to, packed.into_bytes(), Delivery::BestEffort)
.await
{
warn!(error = %e, "transient handshake: failed to send pong");
}
}
_ = shutdown.cancelled() => break,
}
}
});
}
async fn emit_failed(
telemetry: &SharedTelemetrySink,
mediator_did: &str,
stage: HandshakeStage,
cause: &str,
) {
let _ = telemetry
.record(
TelemetryEvent::new(TelemetryKind::MediatorHandshakeFailed)
.with_mediator(mediator_did)
.with_field("stage", JsonValue::from(stage.as_str()))
.with_field("cause", JsonValue::from(cause)),
)
.await;
}
#[cfg(test)]
mod tests {
use crate::messaging::handshake::HandshakeStage;
#[test]
fn transient_handshake_module_compiles() {
let _stage = HandshakeStage::Resolve;
}
}