use tokio::sync::mpsc::UnboundedSender;
use super::ask::ProvisionAsk;
use super::event::{AttemptOutcome, VtaEvent};
use super::intent::VtaIntent;
#[cfg(feature = "tsp")]
const PROVISION_TIMEOUT_SECS: u64 = 60;
#[cfg(feature = "tsp")]
const LIST_SERVERS_TIMEOUT_SECS: u64 = 30;
#[cfg(not(feature = "tsp"))]
fn tsp_feature_missing(vta_did: &str, tsp_mediator_did: &str) -> String {
format!(
"VTA '{vta_did}' advertises a TSP service (`#tsp` → {tsp_mediator_did}), but this \
build of the SDK was compiled without the `tsp` feature, so it cannot provision \
over TSP.\n\n\
Rebuild the setup tool with `--features tsp`, or point provisioning at a VTA \
that also advertises a DIDComm mediator or a `#vta-rest` endpoint."
)
}
#[cfg(feature = "tsp")]
#[allow(clippy::too_many_arguments)]
pub(crate) async fn run_tsp_attempt(
intent: VtaIntent,
vta_did: String,
tsp_mediator_did: String,
rest_url: Option<String>,
setup_did: String,
setup_privkey_mb: String,
ask: ProvisionAsk,
tx: &UnboundedSender<VtaEvent>,
) -> AttemptOutcome {
use super::diagnostics::{DiagCheck, DiagStatus};
use crate::client::VtaClient;
let _ = tx.send(VtaEvent::CheckStart(DiagCheck::AuthenticateTSP));
let client = match VtaClient::connect_tsp(
&setup_did,
&setup_privkey_mb,
&vta_did,
&tsp_mediator_did,
rest_url.clone(),
)
.await
{
Ok(c) => {
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::AuthenticateTSP,
DiagStatus::Ok(format!("TSP session as {setup_did} via {tsp_mediator_did}")),
));
c
}
Err(e) => {
let msg = e.to_string();
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::AuthenticateTSP,
DiagStatus::Failed(msg.clone()),
));
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::ListWebvhServers,
DiagStatus::Skipped("TSP session did not open".into()),
));
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::ProvisionIntegration,
DiagStatus::Skipped("TSP session did not open".into()),
));
return AttemptOutcome::PreAuthFailure(format!(
"Could not open a TSP session to the VTA's `#tsp` mediator \
({tsp_mediator_did}). Confirm the mediator is reachable and that the \
`pnm acl create` command ran successfully for setup DID {setup_did}. \
({msg})"
));
}
};
let outcome = tsp_attempt_body(&client, intent, &setup_did, &setup_privkey_mb, ask, tx).await;
client.shutdown().await;
outcome
}
#[cfg(feature = "tsp")]
async fn tsp_attempt_body(
client: &crate::client::VtaClient,
intent: VtaIntent,
setup_did: &str,
setup_privkey_mb: &str,
ask: ProvisionAsk,
tx: &UnboundedSender<VtaEvent>,
) -> AttemptOutcome {
use super::diagnostics::{DiagCheck, DiagStatus};
use super::intent::VtaReply;
match intent {
VtaIntent::AdminOnly => {
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::ListWebvhServers,
DiagStatus::Skipped("AdminOnly — no VTA-minted DID so no webvh host needed".into()),
));
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::ProvisionIntegration,
DiagStatus::Skipped(
"AdminOnly — setup did:key is the long-term admin credential; \
no template render, no rollover"
.into(),
),
));
AttemptOutcome::Connected(VtaReply::AdminOnly(super::intent::AdminCredentialReply {
admin_did: setup_did.to_string(),
admin_private_key_mb: setup_privkey_mb.to_string(),
}))
}
VtaIntent::AdminRotated => {
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::ListWebvhServers,
DiagStatus::Skipped(
"AdminRotated — no integration DID minted so no webvh host needed".into(),
),
));
let _ = tx.send(VtaEvent::CheckStart(DiagCheck::ProvisionIntegration));
match provision_admin_rotation_over(client, setup_did, setup_privkey_mb, &ask).await {
Ok(reply) => {
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::ProvisionIntegration,
DiagStatus::Ok(format!(
"admin DID rotated: {} (via {})",
reply.admin_did,
ask.admin_template
.as_deref()
.unwrap_or(super::ask::BUILTIN_VTA_ADMIN_TEMPLATE),
)),
));
AttemptOutcome::Connected(VtaReply::AdminOnly(reply))
}
Err(err) => {
let msg = err.to_string();
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::ProvisionIntegration,
DiagStatus::Failed(msg.clone()),
));
AttemptOutcome::PostAuthFailure(format!(
"AdminRotation provisioning failed after auth. ({msg})"
))
}
}
}
VtaIntent::FullSetup => tsp_full_setup(client, setup_did, setup_privkey_mb, ask, tx).await,
}
}
#[cfg(feature = "tsp")]
async fn tsp_full_setup(
client: &crate::client::VtaClient,
setup_did: &str,
setup_privkey_mb: &str,
ask: ProvisionAsk,
tx: &UnboundedSender<VtaEvent>,
) -> AttemptOutcome {
use super::diagnostics::{DiagCheck, DiagStatus};
use super::intent::VtaReply;
use super::runner_didcomm::inject_webvh_vars;
use crate::protocols::did_management::servers::ListWebvhServersResultBody;
let _ = tx.send(VtaEvent::CheckStart(DiagCheck::ListWebvhServers));
let servers = match client
.dispatch_trust_task(
crate::trust_tasks::TASK_WEBVH_SERVERS_LIST_1_0,
serde_json::json!({}),
LIST_SERVERS_TIMEOUT_SECS,
)
.await
.map_err(|e| e.to_string())
.and_then(|payload| {
serde_json::from_value::<ListWebvhServersResultBody>(payload)
.map_err(|e| format!("webvh-server catalogue decode: {e}"))
}) {
Ok(body) => {
let detail = match body.servers.len() {
0 => "no registered servers — serverless path".into(),
1 => format!("1 registered server ({})", body.servers[0].id),
n => format!("{n} registered servers"),
};
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::ListWebvhServers,
DiagStatus::Ok(detail),
));
body.servers
}
Err(msg) => {
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::ListWebvhServers,
DiagStatus::Failed(format!("could not list — continuing serverless ({msg})")),
));
Vec::new()
}
};
let mut ask = ask;
if !ask.integration_template_vars.contains_key("WEBVH_SERVER") {
match servers.len() {
0 => {}
1 => inject_webvh_vars(&mut ask, Some(&servers[0].id), None),
n => {
let msg = format!(
"VTA has {n} registered webvh servers and the provisioning ask names \
none, so the choice is ambiguous. Set `WEBVH_SERVER` in the ask's \
integration_template_vars to the id you want. (The interactive \
picker is driven off VtaEvent::PreflightDone, which the TSP leg \
does not emit.)"
);
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::ProvisionIntegration,
DiagStatus::Failed(msg.clone()),
));
return AttemptOutcome::PostAuthFailure(msg);
}
}
}
let webvh_note = match ask.integration_template_vars.get("WEBVH_SERVER") {
Some(serde_json::Value::String(id)) => format!(", webvh server: {id}"),
_ => ", webvh: serverless".to_string(),
};
let _ = tx.send(VtaEvent::CheckStart(DiagCheck::ProvisionIntegration));
match provision_over(client, setup_did, setup_privkey_mb, &ask).await {
Ok(result) => {
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::ProvisionIntegration,
DiagStatus::Ok(format!(
"admin DID: {} (rolled: {}), integration DID: {}{webvh_note}",
result.admin_did(),
result.summary.admin_rolled_over,
result.integration_did().unwrap_or("(none)"),
)),
));
AttemptOutcome::Connected(VtaReply::Full(Box::new(result)))
}
Err(err) => {
let msg = err.to_string();
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::ProvisionIntegration,
DiagStatus::Failed(msg.clone()),
));
AttemptOutcome::PostAuthFailure(format!("Provisioning failed after auth. ({msg})"))
}
}
}
#[cfg(feature = "tsp")]
async fn provision_over(
client: &crate::client::VtaClient,
setup_did: &str,
setup_privkey_mb: &str,
ask: &ProvisionAsk,
) -> Result<super::result::ProvisionResult, super::error::ProvisionError> {
use super::error::ProvisionError;
use super::result::{decode_nonce_b64url, response_to_result};
let seed = crate::did_key::decode_private_key_multibase(setup_privkey_mb)
.map_err(|e| ProvisionError::SetupKeyMalformed(e.to_string()))?;
let vp = ask.to_builder().sign_with(&seed, setup_did).await?;
let nonce = decode_nonce_b64url(&vp.nonce).map_err(ProvisionError::Armor)?;
let response = dispatch_provision_integration(client, vp, ask).await?;
response_to_result(&seed, nonce, response)
}
#[cfg(feature = "tsp")]
async fn provision_admin_rotation_over(
client: &crate::client::VtaClient,
setup_did: &str,
setup_privkey_mb: &str,
ask: &ProvisionAsk,
) -> Result<super::intent::AdminCredentialReply, super::error::ProvisionError> {
use super::error::ProvisionError;
use super::result::{admin_rotation_response_to_reply, decode_nonce_b64url};
let seed = crate::did_key::decode_private_key_multibase(setup_privkey_mb)
.map_err(|e| ProvisionError::SetupKeyMalformed(e.to_string()))?;
let vp = ask.to_builder().sign_with(&seed, setup_did).await?;
let nonce = decode_nonce_b64url(&vp.nonce).map_err(ProvisionError::Armor)?;
let response = dispatch_provision_integration(client, vp, ask).await?;
admin_rotation_response_to_reply(&seed, nonce, response)
}
#[cfg(feature = "tsp")]
async fn dispatch_provision_integration(
client: &crate::client::VtaClient,
request: crate::provision_integration::BootstrapRequest,
ask: &ProvisionAsk,
) -> Result<
crate::provision_integration::http::ProvisionIntegrationResponse,
super::error::ProvisionError,
> {
use super::error::ProvisionError;
use crate::protocols::provision_integration_management::{
ProvisionSpecVersion, request_body_for_version,
};
let body_struct = crate::provision_integration::http::ProvisionIntegrationRequest {
request,
context: Some(ask.context.clone()),
assertion: None,
vc_validity_seconds: None,
create_context: false,
};
let request_uri = ProvisionSpecVersion::V0_2.request_uri();
let payload = request_body_for_version(&body_struct, request_uri)
.map_err(ProvisionError::Serialization)?;
let reply = client
.dispatch_trust_task(request_uri, payload, PROVISION_TIMEOUT_SECS)
.await
.map_err(ProvisionError::Rpc)?;
serde_json::from_value(reply).map_err(ProvisionError::Serialization)
}
#[cfg(feature = "tsp")]
pub async fn provision_via_tsp(
setup_did: &str,
setup_private_key_mb: &str,
vta_did: &str,
tsp_mediator_did: &str,
ask: &ProvisionAsk,
) -> Result<super::result::ProvisionResult, super::error::ProvisionError> {
use super::error::ProvisionError;
let client = crate::client::VtaClient::connect_tsp(
setup_did,
setup_private_key_mb,
vta_did,
tsp_mediator_did,
None,
)
.await
.map_err(|e| ProvisionError::SessionOpen(e.to_string()))?;
let result = provision_over(&client, setup_did, setup_private_key_mb, ask).await;
client.shutdown().await;
result
}
#[cfg(feature = "tsp")]
pub async fn provision_admin_rotation_via_tsp(
setup_did: &str,
setup_private_key_mb: &str,
vta_did: &str,
tsp_mediator_did: &str,
ask: &ProvisionAsk,
) -> Result<super::intent::AdminCredentialReply, super::error::ProvisionError> {
use super::error::ProvisionError;
let client = crate::client::VtaClient::connect_tsp(
setup_did,
setup_private_key_mb,
vta_did,
tsp_mediator_did,
None,
)
.await
.map_err(|e| ProvisionError::SessionOpen(e.to_string()))?;
let result = provision_admin_rotation_over(&client, setup_did, setup_private_key_mb, ask).await;
client.shutdown().await;
result
}
#[cfg(not(feature = "tsp"))]
#[allow(clippy::too_many_arguments)]
pub(crate) async fn run_tsp_attempt(
_intent: VtaIntent,
vta_did: String,
tsp_mediator_did: String,
_rest_url: Option<String>,
_setup_did: String,
_setup_privkey_mb: String,
_ask: ProvisionAsk,
tx: &UnboundedSender<VtaEvent>,
) -> AttemptOutcome {
use super::diagnostics::{DiagCheck, DiagStatus};
let msg = tsp_feature_missing(&vta_did, &tsp_mediator_did);
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::AuthenticateTSP,
DiagStatus::Skipped("built without the `tsp` feature".into()),
));
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::ListWebvhServers,
DiagStatus::Skipped("no TSP session".into()),
));
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::ProvisionIntegration,
DiagStatus::Skipped("no TSP session".into()),
));
AttemptOutcome::PreAuthFailure(msg)
}
#[cfg(all(test, not(feature = "tsp")))]
mod tests {
use super::*;
#[test]
fn feature_missing_message_names_the_feature_not_the_document() {
let msg = tsp_feature_missing("did:webvh:vta.test", "did:webvh:mediator.test");
assert!(msg.contains("`tsp` feature"), "{msg}");
assert!(msg.contains("--features tsp"), "{msg}");
assert!(msg.contains("did:webvh:mediator.test"), "{msg}");
assert!(
!msg.contains("no endpoints"),
"must not blame the DID document: {msg}"
);
}
}