use std::sync::Arc;
use base64::Engine;
use affinidi_messaging_didcomm::Message;
use affinidi_messaging_didcomm_service::{
DIDCommResponse, DIDCommServiceError, Extension, HandlerContext, ProblemReport,
ServiceProblemReport,
};
use tracing::{info, warn};
use crate::acl::Role;
use crate::error::AppError;
use crate::messaging::auth::auth_from_message;
use crate::operations;
use crate::server::AppState;
use super::router::VtaState;
#[cfg(feature = "webvh")]
use vta_sdk::protocols::did_management;
use vta_sdk::protocols::{
acl_management, audit_management, context_management, credential_exchange, key_management,
seed_management, vta_management,
};
type HandlerResult = Result<Option<DIDCommResponse>, DIDCommServiceError>;
fn handler_err(e: impl std::fmt::Display) -> DIDCommServiceError {
DIDCommServiceError::Handler(e.to_string())
}
fn app_err_to_problem_report(e: &AppError) -> ProblemReport {
match e {
AppError::Conflict(msg) => ProblemReport::conflict(msg.clone()),
AppError::NotFound(msg) => ProblemReport::not_found(msg.clone()),
AppError::Authentication(msg) | AppError::Unauthorized(msg) => {
ProblemReport::unauthorized(msg.clone())
}
AppError::Forbidden(msg) | AppError::StepUpRequired(msg) => ProblemReport {
code: vta_sdk::protocols::problem_report_codes::FORBIDDEN.to_string(),
comment: msg.clone(),
args: Vec::new(),
escalate_to: None,
},
AppError::Validation(msg) => ProblemReport::bad_request(msg.clone()),
_ => ProblemReport::internal_error(e.to_string()),
}
}
fn app_err_to_response(e: AppError) -> DIDCommResponse {
DIDCommResponse::problem_report(app_err_to_problem_report(&e))
}
macro_rules! app_try {
($expr:expr) => {
match $expr {
Ok(v) => v,
Err(err) => return Ok(Some($crate::messaging::handlers::app_err_to_response(err))),
}
};
}
fn response<T: serde::Serialize>(
msg_type: &str,
result: &T,
) -> Result<Option<DIDCommResponse>, DIDCommServiceError> {
let body = serde_json::to_value(result).map_err(handler_err)?;
Ok(Some(DIDCommResponse::new(msg_type, body)))
}
#[derive(Clone, Copy)]
enum Gate {
None,
Write,
Manage,
Admin,
SuperAdmin,
}
impl Gate {
fn check(self, auth: &crate::auth::AuthClaims) -> Result<(), AppError> {
match self {
Gate::None => Ok(()),
Gate::Write => auth.require_write(),
Gate::Manage => auth.require_manage(),
Gate::Admin => auth.require_admin(),
Gate::SuperAdmin => auth.require_super_admin(),
}
}
}
async fn dispatch<B, R>(
message: Message,
state: &Arc<VtaState>,
gate: Gate,
result_type: &str,
op: impl AsyncFnOnce(crate::auth::AuthClaims, B) -> Result<R, AppError>,
) -> HandlerResult
where
B: serde::de::DeserializeOwned,
R: serde::Serialize,
{
let auth = app_try!(auth_from_message(&message, &state.acl_ks).await);
app_try!(gate.check(&auth));
let body: B = serde_json::from_value(message.body).map_err(handler_err)?;
let result = app_try!(op(auth, body).await);
response(result_type, &result)
}
async fn dispatch_no_body<R>(
message: Message,
state: &Arc<VtaState>,
gate: Gate,
result_type: &str,
op: impl AsyncFnOnce(crate::auth::AuthClaims) -> Result<R, AppError>,
) -> HandlerResult
where
R: serde::Serialize,
{
let auth = app_try!(auth_from_message(&message, &state.acl_ks).await);
app_try!(gate.check(&auth));
let result = app_try!(op(auth).await);
response(result_type, &result)
}
macro_rules! didcomm_handler {
($name:ident, $gate:expr, $result:expr, $body:ty,
|$s:ident, $auth:ident, $b:ident| $op:expr $(,)?) => {
pub async fn $name(
_ctx: HandlerContext,
message: Message,
Extension(state): Extension<Arc<VtaState>>,
) -> HandlerResult {
dispatch(message, &state, $gate, $result, async |$auth, $b: $body| {
let $s = &state;
$op
})
.await
}
};
($name:ident, $gate:expr, $result:expr, |$s:ident, $auth:ident| $op:expr $(,)?) => {
pub async fn $name(
_ctx: HandlerContext,
message: Message,
Extension(state): Extension<Arc<VtaState>>,
) -> HandlerResult {
dispatch_no_body(message, &state, $gate, $result, async |$auth| {
let $s = &state;
$op
})
.await
}
};
(resolver $name:ident, $gate:expr, $result:expr, $body:ty,
|$s:ident, $auth:ident, $b:ident, $res:ident| $op:expr $(,)?) => {
pub async fn $name(
_ctx: HandlerContext,
message: Message,
Extension(state): Extension<Arc<VtaState>>,
) -> HandlerResult {
let $res = state
.did_resolver
.as_ref()
.ok_or_else(|| handler_err("DID resolver not available"))?;
dispatch(message, &state, $gate, $result, async |$auth, $b: $body| {
let $s = &state;
$op
})
.await
}
};
}
pub(crate) const TRUST_TASK_ENVELOPE_TYPE: &str =
"https://trusttasks.org/binding/didcomm/0.1/envelope";
pub async fn handle_trust_task(
_ctx: HandlerContext,
message: Message,
Extension(app_state): Extension<AppState>,
) -> HandlerResult {
let body = serde_json::to_vec(&message.body).map_err(handler_err)?;
let response = match auth_from_message(&message, &app_state.acl_ks).await {
Ok(auth) => crate::trust_tasks::dispatch_trust_task_core(&app_state, &auth, &body).await,
Err(e) => crate::trust_tasks::reject_trust_task(
&body,
trust_tasks_rs::RejectReason::PermissionDenied {
reason: e.to_string(),
},
),
};
let doc: serde_json::Value = serde_json::from_slice(&response.body).map_err(handler_err)?;
Ok(Some(DIDCommResponse::new(TRUST_TASK_ENVELOPE_TYPE, doc)))
}
didcomm_handler!(
handle_create_key,
Gate::Admin,
key_management::CREATE_KEY_RESULT,
key_management::create::CreateKeyBody,
|s, auth, body| operations::keys::create_key(
&s.keys_ks,
&s.contexts_ks,
&s.seed_store,
&s.audit_ks,
&auth,
operations::keys::CreateKeyParams {
key_type: body.key_type,
derivation_path: if body.derivation_path.is_empty() {
None
} else {
Some(body.derivation_path)
},
key_id: None,
mnemonic: body.mnemonic,
label: body.label,
context_id: body.context_id,
},
"didcomm",
)
.await
);
didcomm_handler!(
handle_get_key,
Gate::None,
key_management::GET_KEY_RESULT,
key_management::get::GetKeyBody,
|s, auth, body| operations::keys::get_key(&s.keys_ks, &auth, &body.key_id, "didcomm").await
);
didcomm_handler!(
handle_list_keys,
Gate::None,
key_management::LIST_KEYS_RESULT,
key_management::list::ListKeysBody,
|s, auth, body| operations::keys::list_keys(
&s.keys_ks,
&auth,
operations::keys::ListKeysParams {
offset: body.offset,
limit: body.limit,
status: body.status,
context_id: body.context_id,
},
"didcomm",
)
.await
);
didcomm_handler!(
handle_rename_key,
Gate::Admin,
key_management::RENAME_KEY_RESULT,
key_management::rename::RenameKeyBody,
|s, auth, body| operations::keys::rename_key(
&s.keys_ks,
&s.audit_ks,
&auth,
&body.key_id,
&body.new_key_id,
"didcomm",
)
.await
);
didcomm_handler!(
handle_revoke_key,
Gate::Admin,
key_management::REVOKE_KEY_RESULT,
key_management::revoke::RevokeKeyBody,
|s, auth, body| operations::keys::revoke_key(
&s.keys_ks,
&s.imported_ks,
&s.audit_ks,
&auth,
&body.key_id,
"didcomm",
)
.await
);
didcomm_handler!(
handle_get_key_secret,
Gate::Admin,
key_management::GET_KEY_SECRET_RESULT,
key_management::secret::GetKeySecretBody,
|s, auth, body| operations::keys::get_key_secret(
&s.keys_ks,
&s.imported_ks,
&s.seed_store,
&s.audit_ks,
&auth,
&body.key_id,
"didcomm",
)
.await
);
didcomm_handler!(
handle_sign_request,
Gate::Write,
key_management::SIGN_RESULT,
key_management::sign::SignRequestBody,
|s, auth, body| {
let payload = base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(&body.payload)
.map_err(|e| AppError::Validation(format!("invalid base64url payload: {e}")))?;
operations::keys::sign_payload(
&s.keys_ks,
&s.imported_ks,
&s.seed_store,
&auth,
&body.key_id,
&payload,
&body.algorithm,
"didcomm",
)
.await
}
);
didcomm_handler!(
handle_list_seeds,
Gate::Admin,
seed_management::LIST_SEEDS_RESULT,
|s, _auth| operations::seeds::list_seeds(&s.keys_ks, "didcomm").await
);
didcomm_handler!(
handle_rotate_seed,
Gate::Admin,
seed_management::ROTATE_SEED_RESULT,
seed_management::rotate::RotateSeedBody,
|s, auth, body| operations::seeds::rotate_seed(
&s.keys_ks,
&s.imported_ks,
&s.seed_store,
&s.audit_ks,
&auth.did,
body.mnemonic.as_deref(),
"didcomm",
)
.await
);
didcomm_handler!(
handle_create_context,
Gate::Admin,
context_management::CREATE_CONTEXT_RESULT,
context_management::create::CreateContextBody,
|s, auth, body| operations::contexts::create_context(
&s.contexts_ks,
&auth,
&body.id,
body.name,
body.description,
body.parent,
"didcomm",
)
.await
);
didcomm_handler!(
handle_get_context,
Gate::None,
context_management::GET_CONTEXT_RESULT,
context_management::get::GetContextBody,
|s, auth, body| operations::contexts::get_context_op(
&s.contexts_ks,
&auth,
&body.id,
"didcomm"
)
.await
);
didcomm_handler!(
handle_list_contexts,
Gate::None,
context_management::LIST_CONTEXTS_RESULT,
|s, auth| operations::contexts::list_contexts(&s.contexts_ks, &auth, "didcomm").await
);
didcomm_handler!(
handle_update_context,
Gate::SuperAdmin,
context_management::UPDATE_CONTEXT_RESULT,
context_management::update::UpdateContextBody,
|s, auth, body| operations::contexts::update_context(
&s.contexts_ks,
&auth,
&body.id,
operations::contexts::UpdateContextParams {
name: body.name,
did: body.did,
description: body.description,
},
"didcomm",
)
.await
);
didcomm_handler!(
handle_update_context_did,
Gate::Admin,
context_management::UPDATE_CONTEXT_DID_RESULT,
context_management::update_did::UpdateContextDidBody,
|s, auth, body| operations::contexts::update_context_did(
&s.contexts_ks,
&auth,
&body.id,
body.did,
"didcomm",
)
.await
);
didcomm_handler!(
handle_preview_delete_context,
Gate::Admin,
context_management::PREVIEW_DELETE_CONTEXT_RESULT,
context_management::delete::DeleteContextPreviewBody,
|s, auth, body| operations::contexts::preview_delete_context(
&s.contexts_ks,
&s.keys_ks,
&s.acl_ks,
&s.did_templates_ks,
#[cfg(feature = "webvh")]
&s.webvh_ks,
&auth,
&body.id,
"didcomm",
)
.await
);
didcomm_handler!(
handle_delete_context,
Gate::Admin,
context_management::DELETE_CONTEXT_RESULT,
context_management::delete::DeleteContextBody,
|s, auth, body| {
let ks = operations::Keyspaces::from_vta_state(s);
operations::contexts::delete_context(&ks, &auth, &body.id, body.force, "didcomm").await
}
);
didcomm_handler!(
handle_create_acl,
Gate::Manage,
acl_management::CREATE_ACL_RESULT,
acl_management::create::CreateAclBody,
|s, auth, body| {
let role = Role::parse(&body.role)?;
operations::acl::create_acl(
&s.acl_ks,
&s.audit_ks,
&s.contexts_ks,
&auth,
&body.did,
role,
body.label,
body.allowed_contexts,
body.expires_at,
body.step_up_approver,
body.step_up_require,
"didcomm",
)
.await
}
);
pub async fn handle_swap_acl(
_ctx: HandlerContext,
message: Message,
Extension(state): Extension<Arc<VtaState>>,
) -> HandlerResult {
let is_canonical = message.typ == acl_management::ACL_SWAP_KEY;
let auth = app_try!(auth_from_message(&message, &state.acl_ks).await);
let (presentation, claimed_new_subject) = if is_canonical {
let body: vta_sdk::protocols::acl_management::swap::SwapKeyBody =
serde_json::from_value(message.body).map_err(handler_err)?;
if body.current_subject != auth.did {
return Err(handler_err(format!(
"acl/swap-key: currentSubject {} does not equal authenticated sender {}",
body.current_subject, auth.did
)));
}
(body.link_proof, Some(body.new_subject))
} else {
let body: vta_sdk::protocols::acl_management::swap::SwapAclBody =
serde_json::from_value(message.body).map_err(handler_err)?;
(body.presentation, None)
};
if !matches!(
crate::operations::step_up::resolve_step_up(
&state.config,
&state.acl_ks,
crate::operations::step_up::op::ACL_SWAP_KEY,
&auth.did,
true, )
.await,
crate::operations::step_up::StepUpDecision::Allow
) {
return Ok(Some(app_err_to_response(AppError::StepUpRequired(
"acl/swap-key requires a stepped-up (AAL2) session under this VTA's step-up \
policy. DIDComm sender-authentication is AAL1 and cannot be elevated in-band; \
perform this self-service rotation over the authenticated REST session, which \
can complete step-up."
.to_string(),
))));
}
let did_resolver = state
.did_resolver
.as_ref()
.ok_or_else(|| handler_err("DID resolver not available"))?;
let vta_did = {
let config = state.config.read().await;
config
.vta_did
.clone()
.ok_or_else(|| handler_err("VTA DID not configured"))?
};
let result = app_try!(
operations::acl::swap_acl(
&state.acl_ks,
&state.audit_ks,
&auth,
&presentation,
did_resolver,
&vta_did,
"didcomm",
)
.await
);
if let Some(claimed) = claimed_new_subject
&& claimed != result.did
{
return Err(handler_err(format!(
"acl/swap-key: newSubject {} does not match verified VP holder {}",
claimed, result.did
)));
}
let response_type = if is_canonical {
acl_management::ACL_SWAP_KEY_RESPONSE
} else {
acl_management::SWAP_ACL_RESULT
};
response(response_type, &result)
}
didcomm_handler!(
handle_get_acl,
Gate::Manage,
acl_management::GET_ACL_RESULT,
acl_management::get::GetAclBody,
|s, auth, body| operations::acl::get_acl(&s.acl_ks, &auth, &body.did, "didcomm").await
);
didcomm_handler!(
handle_list_acl,
Gate::Manage,
acl_management::LIST_ACL_RESULT,
acl_management::list::ListAclBody,
|s, auth, body| operations::acl::list_acl(&s.acl_ks, &auth, body.context.as_deref(), "didcomm")
.await
);
didcomm_handler!(
handle_update_acl,
Gate::Manage,
acl_management::UPDATE_ACL_RESULT,
acl_management::update::UpdateAclBody,
|s, auth, body| {
let role = match body.role {
Some(r) => Some(Role::parse(&r)?),
None => None,
};
operations::acl::update_acl(
&s.acl_ks,
&s.audit_ks,
&s.contexts_ks,
&auth,
&body.did,
operations::acl::UpdateAclParams {
role,
label: body.label,
allowed_contexts: body.allowed_contexts,
step_up_approver: body.step_up_approver,
step_up_require: body.step_up_require,
},
"didcomm",
)
.await
}
);
didcomm_handler!(
handle_delete_acl,
Gate::Manage,
acl_management::DELETE_ACL_RESULT,
acl_management::delete::DeleteAclBody,
|s, auth, body| operations::acl::delete_acl(
&s.acl_ks,
&s.audit_ks,
&auth,
&body.did,
"didcomm"
)
.await
);
didcomm_handler!(
handle_list_logs,
Gate::Admin,
audit_management::LIST_LOGS_RESULT,
audit_management::list::ListAuditLogsBody,
|s, auth, body| operations::audit::list_audit_logs(&s.audit_ks, &auth, &body, "didcomm").await
);
didcomm_handler!(
handle_get_retention,
Gate::Admin,
audit_management::GET_RETENTION_RESULT,
|s, auth| operations::audit::get_retention(&s.config, &auth, "didcomm").await
);
didcomm_handler!(
handle_update_retention,
Gate::SuperAdmin,
audit_management::UPDATE_RETENTION_RESULT,
audit_management::retention::UpdateRetentionBody,
|s, auth, body| operations::audit::update_retention(
&s.config,
&s.audit_ks,
&auth,
body.retention_days,
"didcomm",
)
.await
);
didcomm_handler!(
handle_get_config,
Gate::None,
vta_management::GET_CONFIG_RESULT,
|s, auth| operations::config::get_config(&s.config, &auth, "didcomm").await
);
didcomm_handler!(
handle_update_config,
Gate::SuperAdmin,
vta_management::UPDATE_CONFIG_RESULT,
vta_management::update_config::UpdateConfigBody,
|s, auth, body| operations::config::update_config(
&s.config,
&auth,
operations::config::UpdateConfigParams {
vta_did: body.vta_did,
vta_name: body.vta_name,
public_url: body.public_url,
},
"didcomm",
)
.await
);
#[cfg(feature = "webvh")]
didcomm_handler!(
resolver handle_create_did_webvh, Gate::None, did_management::CREATE_DID_WEBVH_RESULT,
did_management::create::CreateDidWebvhBody,
|s, auth, body, did_resolver| {
let config = s.config.read().await;
let deps =
operations::did_webvh::CreateDidWebvhDeps::from_vta_state(s, &config, did_resolver);
operations::did_webvh::create_did_webvh(&deps, &auth, body.into(), "didcomm").await
}
);
#[cfg(feature = "webvh")]
didcomm_handler!(
handle_get_did_webvh,
Gate::None,
did_management::GET_DID_WEBVH_RESULT,
did_management::get::GetDidWebvhBody,
|s, auth, body| operations::did_webvh::get_did_webvh(&s.webvh_ks, &auth, &body.did, "didcomm")
.await
);
#[cfg(feature = "webvh")]
didcomm_handler!(
handle_get_did_webvh_log,
Gate::None,
did_management::GET_DID_WEBVH_LOG_RESULT,
did_management::get::GetDidWebvhBody,
|s, auth, body| operations::did_webvh::get_did_webvh_log(
&s.webvh_ks,
&auth,
&body.did,
"didcomm"
)
.await
);
#[cfg(feature = "webvh")]
didcomm_handler!(
handle_list_dids_webvh,
Gate::None,
did_management::LIST_DIDS_WEBVH_RESULT,
did_management::list::ListDidsWebvhBody,
|s, auth, body| operations::did_webvh::list_dids_webvh(
&s.webvh_ks,
&auth,
body.context_id.as_deref(),
body.server_id.as_deref(),
"didcomm",
)
.await
);
#[cfg(feature = "webvh")]
didcomm_handler!(
resolver handle_delete_did_webvh, Gate::None, did_management::DELETE_DID_WEBVH_RESULT,
did_management::delete::DeleteDidWebvhBody,
|s, auth, body, did_resolver| {
let vta_did = s.config.read().await.vta_did.clone();
let deps = operations::did_webvh::WebvhDeps::from_vta_state(s, did_resolver);
operations::did_webvh::delete_did_webvh(&deps, &auth, &body.did, vta_did.as_deref(), "didcomm")
.await
}
);
#[cfg(feature = "webvh")]
didcomm_handler!(
resolver handle_add_webvh_server, Gate::None, did_management::ADD_WEBVH_SERVER_RESULT,
did_management::servers::AddWebvhServerBody,
|s, auth, body, did_resolver| operations::did_webvh::add_webvh_server(
&s.webvh_ks,
&auth,
&body.id,
&body.did,
body.label,
did_resolver,
"didcomm",
)
.await
);
#[cfg(feature = "webvh")]
didcomm_handler!(
handle_list_webvh_servers,
Gate::None,
did_management::LIST_WEBVH_SERVERS_RESULT,
|s, auth| operations::did_webvh::list_webvh_servers(&s.webvh_ks, &auth, "didcomm").await
);
#[cfg(feature = "webvh")]
didcomm_handler!(
resolver handle_list_webvh_server_domains, Gate::None,
did_management::LIST_WEBVH_SERVER_DOMAINS_RESULT,
did_management::servers::ListWebvhServerDomainsBody,
|s, auth, body, did_resolver| {
let vta_did = s.config.read().await.vta_did.clone();
let deps = operations::did_webvh::WebvhDeps::from_vta_state(s, did_resolver);
operations::did_webvh::list_webvh_server_domains(
&deps,
&auth,
vta_did.as_deref(),
&body.server_id,
)
.await
}
);
#[cfg(feature = "webvh")]
didcomm_handler!(
handle_update_webvh_server,
Gate::None,
did_management::UPDATE_WEBVH_SERVER_RESULT,
did_management::servers::UpdateWebvhServerBody,
|s, auth, body| operations::did_webvh::update_webvh_server(
&s.webvh_ks,
&auth,
&body.id,
body.label,
"didcomm",
)
.await
);
#[cfg(feature = "webvh")]
didcomm_handler!(
handle_remove_webvh_server,
Gate::None,
did_management::REMOVE_WEBVH_SERVER_RESULT,
did_management::servers::RemoveWebvhServerBody,
|s, auth, body| operations::did_webvh::remove_webvh_server(
&s.webvh_ks,
&auth,
&body.id,
"didcomm"
)
.await
);
#[cfg(feature = "webvh")]
didcomm_handler!(
resolver handle_register_did_with_server, Gate::None,
did_management::REGISTER_DID_WITH_SERVER_RESULT,
did_management::servers::RegisterDidWithServerBody,
|s, auth, body, did_resolver| {
let vta_did = s.config.read().await.vta_did.clone();
let deps = operations::did_webvh::WebvhDeps::from_vta_state(s, did_resolver);
let result = operations::did_webvh::register_did_with_server(
&deps,
&auth,
operations::did_webvh::RegisterDidWithServerParams {
did: body.did,
server_id: body.server_id,
force: body.force,
domain: body.domain,
},
vta_did.as_deref(),
"didcomm",
)
.await
.map_err(register_err_to_app_error)?;
Ok(did_management::servers::RegisterDidWithServerResultBody {
did: result.did,
server_id: result.server_id,
log_entry_count: result.log_entry_count,
})
}
);
#[cfg(feature = "webvh")]
fn register_err_to_app_error(e: operations::did_webvh::RegisterDidWithServerError) -> AppError {
use operations::did_webvh::RegisterDidWithServerError as E;
match e {
E::Auth(msg) => AppError::Forbidden(msg),
E::DidNotFound(msg) | E::ServerNotFound(msg) | E::LogMissing(msg) => {
AppError::NotFound(msg)
}
E::AlreadyServerManaged { .. } | E::Conflict(_) => AppError::Conflict(e.to_string()),
E::Transport(msg) | E::Publish(msg) => AppError::Internal(format!("publish: {msg}")),
E::DidUrlParse { .. } => AppError::Validation(e.to_string()),
E::Storage(msg) => AppError::Internal(msg),
}
}
#[cfg(feature = "tee")]
pub async fn handle_tee_status(
_ctx: HandlerContext,
_message: Message,
Extension(state): Extension<Arc<VtaState>>,
) -> HandlerResult {
let tee_state = state
.tee_state
.as_ref()
.ok_or_else(|| handler_err("TEE attestation is not enabled on this VTA"))?;
let status = operations::attestation::get_tee_status(tee_state);
response(
vta_sdk::protocols::attestation_management::GET_TEE_STATUS_RESULT,
&status,
)
}
#[cfg(feature = "tee")]
pub async fn handle_request_attestation(
_ctx: HandlerContext,
message: Message,
Extension(state): Extension<Arc<VtaState>>,
) -> HandlerResult {
let tee_state = state
.tee_state
.as_ref()
.ok_or_else(|| handler_err("TEE attestation is not enabled on this VTA"))?;
let body: crate::tee::types::AttestationRequest =
serde_json::from_value(message.body).map_err(handler_err)?;
let result = app_try!(
operations::attestation::generate_attestation_report(tee_state, &state.config, &body.nonce)
.await
);
response(
vta_sdk::protocols::attestation_management::ATTESTATION_RESULT,
&result,
)
}
pub async fn handle_restart(
_ctx: HandlerContext,
message: Message,
Extension(state): Extension<Arc<VtaState>>,
) -> HandlerResult {
let auth = app_try!(auth_from_message(&message, &state.acl_ks).await);
app_try!(auth.require_super_admin());
let _ = crate::audit::record(
&state.audit_ks,
"vta.restart",
&auth.did,
None,
"success",
Some("didcomm"),
None,
)
.await;
crate::server::trigger_restart(&state.restart_tx);
response(
vta_sdk::protocols::vta_management::RESTART_RESULT,
&vta_sdk::protocols::vta_management::restart::RestartResult {
status: "restarting".into(),
},
)
}
pub async fn handle_backup_export(
_ctx: HandlerContext,
message: Message,
Extension(state): Extension<Arc<VtaState>>,
) -> HandlerResult {
let auth = app_try!(auth_from_message(&message, &state.acl_ks).await);
app_try!(auth.require_super_admin());
let body: vta_sdk::protocols::backup_management::types::ExportRequest =
serde_json::from_value(message.body).map_err(handler_err)?;
let config = state.config.read().await;
let ks = operations::Keyspaces::from_vta_state(&state);
let envelope = app_try!(
operations::backup::export_backup(
&ks,
&*state.seed_store,
&config,
&auth,
&body.password,
body.include_audit,
)
.await
);
let _ = crate::audit::record(
&state.audit_ks,
"backup.export",
&auth.did,
None,
"success",
Some("didcomm"),
None,
)
.await;
info!(
ciphertext_bytes = envelope.ciphertext.len(),
"backup export DIDComm response size"
);
response(
vta_sdk::protocols::backup_management::EXPORT_BACKUP_RESULT,
&envelope,
)
}
pub async fn handle_backup_import(
_ctx: HandlerContext,
message: Message,
Extension(state): Extension<Arc<VtaState>>,
) -> HandlerResult {
let auth = app_try!(auth_from_message(&message, &state.acl_ks).await);
app_try!(auth.require_super_admin());
let body: vta_sdk::protocols::backup_management::types::ImportRequest =
serde_json::from_value(message.body).map_err(handler_err)?;
if !body.confirm {
let (_payload, preview) =
app_try!(operations::backup::preview_import(&body.backup, &body.password).await);
return response(
vta_sdk::protocols::backup_management::IMPORT_BACKUP_RESULT,
&preview,
);
}
let payload = app_try!(operations::backup::decrypt_backup(
&body.backup,
&body.password
));
let ks = operations::Keyspaces::from_vta_state(&state);
let result = app_try!(
operations::backup::apply_import(
&payload,
&ks,
&state.seed_store,
&state.config,
None, )
.await
);
let _ = crate::audit::record(
&state.audit_ks,
"backup.import",
&auth.did,
payload.config.vta_did.as_deref(),
"success",
Some("didcomm"),
None,
)
.await;
crate::server::trigger_restart(&state.restart_tx);
response(
vta_sdk::protocols::backup_management::IMPORT_BACKUP_RESULT,
&result,
)
}
pub async fn handle_problem_report(_ctx: HandlerContext, message: Message) -> HandlerResult {
let code = message
.body
.get("code")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
let comment = message
.body
.get("comment")
.and_then(|v| v.as_str())
.unwrap_or("no details provided");
let from = message.from.as_deref().unwrap_or("unknown");
let thid = message.thid.as_deref().unwrap_or("none");
warn!(from, code, comment, thid, msg_type = %message.typ, "received problem-report");
Ok(None)
}
pub async fn handle_discover_capabilities(
_ctx: HandlerContext,
_message: Message,
Extension(state): Extension<Arc<VtaState>>,
) -> HandlerResult {
let config = state.config.read().await;
let features = vta_sdk::protocols::discovery::FeaturesInfo {
webvh: cfg!(feature = "webvh"),
didcomm: cfg!(feature = "didcomm"),
tee: cfg!(feature = "tee"),
rest: cfg!(feature = "rest"),
};
let services = vta_sdk::protocols::discovery::ServicesInfo {
rest: config.services.rest,
didcomm: config.services.didcomm,
};
#[cfg(feature = "webvh")]
let webvh_servers = {
let servers = app_try!(crate::webvh_store::list_servers(&state.webvh_ks).await);
servers
.into_iter()
.map(|s| vta_sdk::protocols::discovery::WebvhServerInfo {
id: s.id,
label: s.label,
})
.collect()
};
#[cfg(not(feature = "webvh"))]
let webvh_servers: Vec<vta_sdk::protocols::discovery::WebvhServerInfo> = vec![];
let mut did_creation_modes = vec!["vta-built".to_string()];
if cfg!(feature = "webvh") {
did_creation_modes.push("template".to_string());
did_creation_modes.push("final".to_string());
did_creation_modes.push("user-specified-keys".to_string());
}
let result = vta_sdk::protocols::discovery::CapabilitiesResponse {
version: env!("CARGO_PKG_VERSION").to_string(),
features,
services,
webvh_servers,
did_creation_modes,
};
response(
vta_sdk::protocols::discovery::DISCOVER_CAPABILITIES_RESULT,
&result,
)
}
#[cfg(feature = "webvh")]
pub async fn handle_provision_integration(
_ctx: HandlerContext,
message: Message,
Extension(state): Extension<Arc<VtaState>>,
) -> HandlerResult {
let auth = app_try!(auth_from_message(&message, &state.acl_ks).await);
let request_raw = message.body.get("request").cloned();
let body: vta_sdk::protocols::provision_integration_management::request::ProvisionIntegrationRequest =
serde_json::from_value(message.body).map_err(handler_err)?;
let request_raw = match request_raw {
Some(v) => v,
None => {
return Ok(Some(app_err_to_response(AppError::Validation(
"provision-integration request missing 'request' field".into(),
))));
}
};
let verified = match vta_sdk::provision_integration::BootstrapRequest::verify_value(request_raw)
{
Ok(v) => v,
Err(e) => {
return Ok(Some(app_err_to_response(AppError::Validation(format!(
"verify BootstrapRequest VP: {e}"
)))));
}
};
let assertion_mode = body
.assertion
.map(|m| match m {
vta_sdk::provision_integration::http::AssertionMode::DidSigned => {
operations::provision_integration::AssertionMode::DidSigned
}
vta_sdk::provision_integration::http::AssertionMode::PinnedOnly => {
operations::provision_integration::AssertionMode::PinnedOnly
}
})
.unwrap_or_default();
let vc_validity = body.vc_validity_seconds.map(chrono::Duration::seconds);
let deps = operations::provision_integration::ProvisionIntegrationDeps::from(state.as_ref());
let (context, context_created) =
match operations::provision_integration::resolve_target_context(
&auth,
&deps.contexts_ks,
body.context,
body.create_context,
)
.await
{
Ok(v) => v,
Err(operations::provision_integration::ResolveContextError::Ambiguous(
operations::provision_integration::AmbiguousContext {
candidates,
message,
},
)) => {
let report = ProblemReport {
code: vta_sdk::protocols::problem_report_codes::PROVISION_CONTEXT_REQUIRED
.to_string(),
comment: message,
args: candidates,
escalate_to: None,
};
return Ok(Some(DIDCommResponse::problem_report(report)));
}
Err(operations::provision_integration::ResolveContextError::Op(e)) => {
return Ok(Some(app_err_to_response(e)));
}
};
let output = app_try!(
operations::provision_integration::provision_integration(
&deps,
&auth,
operations::provision_integration::ProvisionIntegrationParams {
request: verified,
context,
assertion_mode,
vc_validity,
},
)
.await
);
let result = vta_sdk::provision_integration::http::ProvisionIntegrationResponse {
bundle: output.armored,
digest: output.digest,
summary: vta_sdk::provision_integration::http::ProvisionSummary {
client_did: output.summary.client_did,
admin_did: output.summary.admin_did,
admin_rolled_over: output.summary.admin_rolled_over,
integration_did: output.summary.integration_did,
template_name: output.summary.template_name,
template_kind: output.summary.template_kind,
admin_template_name: output.summary.admin_template_name,
bundle_id_hex: output.summary.bundle_id_hex,
secret_count: output.summary.secret_count,
output_count: output.summary.output_count,
webvh_server_id: output.summary.webvh_server_id,
context_created,
},
};
let result_uri =
vta_sdk::protocols::provision_integration_management::result_uri_for(&message.typ);
info!(
from = %auth.did,
admin_did = %result.summary.admin_did,
admin_rolled_over = result.summary.admin_rolled_over,
bundle_id = %result.summary.bundle_id_hex,
request_uri = %message.typ,
result_uri,
"provision-integration completed via DIDComm"
);
let body = vta_sdk::protocols::provision_integration_management::response_body_for_version(
&result,
&message.typ,
)
.map_err(handler_err)?;
response(result_uri, &body)
}
#[cfg(feature = "webvh")]
#[derive(Debug, serde::Deserialize)]
struct WebvhUpdateEnvelope<B> {
#[allow(dead_code)]
context_id: String,
scid: String,
body: B,
}
#[cfg(feature = "webvh")]
pub async fn handle_update_did_webvh(
_ctx: HandlerContext,
message: Message,
Extension(state): Extension<Arc<VtaState>>,
) -> HandlerResult {
let auth = app_try!(auth_from_message(&message, &state.acl_ks).await);
let env: WebvhUpdateEnvelope<vta_sdk::protocols::did_management::update::UpdateDidWebvhBody> =
serde_json::from_value(message.body).map_err(handler_err)?;
let did_resolver = state
.did_resolver
.as_ref()
.ok_or_else(|| handler_err("DID resolver not available"))?;
let witnesses = env
.body
.witnesses
.map(serde_json::from_value)
.transpose()
.map_err(handler_err)?;
let opts = operations::did_webvh::UpdateDidWebvhOptions {
document: env.body.document,
pre_rotation_count: env.body.pre_rotation_count,
witnesses,
watchers: env.body.watchers,
ttl: env.body.ttl,
label: env.body.label,
expected_version_id: env.body.expected_version_id,
};
let vta_did = state.config.read().await.vta_did.clone();
let deps = operations::did_webvh::WebvhDeps::from_vta_state(&state, did_resolver);
let result = app_try!(
operations::did_webvh::update_did_webvh(
&deps,
&auth,
&env.scid,
opts,
vta_did.as_deref(),
"didcomm",
)
.await
.map_err(crate::error::AppError::from)
);
let body = vta_sdk::protocols::did_management::update::UpdateDidWebvhResultBody {
did: result.did,
new_version_id: result.new_version_id,
new_scid: result.new_scid,
new_log_entry: result.new_log_entry,
update_keys_count: result.update_keys_count,
pre_rotation_key_count: result.pre_rotation_key_count,
serverless: result.serverless,
};
response(
vta_sdk::protocols::did_management::UPDATE_DID_WEBVH_RESULT,
&body,
)
}
#[cfg(feature = "webvh")]
pub async fn handle_rotate_did_webvh_keys(
_ctx: HandlerContext,
message: Message,
Extension(state): Extension<Arc<VtaState>>,
) -> HandlerResult {
let auth = app_try!(auth_from_message(&message, &state.acl_ks).await);
let env: WebvhUpdateEnvelope<
vta_sdk::protocols::did_management::update::RotateDidWebvhKeysBody,
> = serde_json::from_value(message.body).map_err(handler_err)?;
let did_resolver = state
.did_resolver
.as_ref()
.ok_or_else(|| handler_err("DID resolver not available"))?;
let opts = operations::did_webvh::RotateDidWebvhKeysOptions {
pre_rotation_count: env.body.pre_rotation_count,
label: env.body.label,
};
let vta_did = state.config.read().await.vta_did.clone();
let deps = operations::did_webvh::WebvhDeps::from_vta_state(&state, did_resolver);
let result = app_try!(
operations::did_webvh::rotate_did_webvh_keys(
&deps,
&auth,
&env.scid,
opts,
vta_did.as_deref(),
"didcomm",
)
.await
.map_err(crate::error::AppError::from)
);
let body = vta_sdk::protocols::did_management::update::UpdateDidWebvhResultBody {
did: result.did,
new_version_id: result.new_version_id,
new_scid: result.new_scid,
new_log_entry: result.new_log_entry,
update_keys_count: result.update_keys_count,
pre_rotation_key_count: result.pre_rotation_key_count,
serverless: result.serverless,
};
response(
vta_sdk::protocols::did_management::ROTATE_DID_WEBVH_KEYS_RESULT,
&body,
)
}
pub(crate) const STEP_UP_APPROVE_REQUEST_TYPE: &str =
"https://trusttasks.org/vta/step-up/approve-request/1.0";
pub(crate) const STEP_UP_APPROVE_RESPONSE_TYPE: &str =
"https://trusttasks.org/vta/step-up/approve-response/1.0";
pub(crate) const STEP_UP_APPROVE_REQUEST_CANONICAL: &str =
"https://trusttasks.org/spec/auth/step-up/approve-request/0.1";
pub(crate) const STEP_UP_APPROVE_RESPONSE_CANONICAL: &str =
"https://trusttasks.org/spec/auth/step-up/approve-response/0.1";
#[derive(serde::Deserialize)]
struct StepUpApproveRequestBody {
#[serde(alias = "rpDid")]
rp_did: String,
nonce: String,
}
#[derive(serde::Serialize)]
struct StepUpApproveResponseBody {
approval_token: String,
}
pub async fn handle_step_up_approve(
_ctx: HandlerContext,
message: Message,
Extension(state): Extension<Arc<VtaState>>,
) -> HandlerResult {
let holder_did = match message.from.as_deref() {
Some(d) => d.split('#').next().unwrap_or(d).to_string(),
None => {
return Ok(Some(app_err_to_response(AppError::Authentication(
"step-up approve request has no authenticated sender".into(),
))));
}
};
let response_type = if message.typ == STEP_UP_APPROVE_REQUEST_CANONICAL {
STEP_UP_APPROVE_RESPONSE_CANONICAL
} else {
STEP_UP_APPROVE_RESPONSE_TYPE
};
let body: StepUpApproveRequestBody =
serde_json::from_value(message.body).map_err(handler_err)?;
if !operations::step_up_approval::step_up_policy_approve(&holder_did, &body.rp_did) {
return Ok(Some(app_err_to_response(AppError::Forbidden(format!(
"step-up approval denied for holder {holder_did} at {}",
body.rp_did
)))));
}
let vta_did = match state.config.read().await.vta_did.clone() {
Some(d) => d,
None => {
return Ok(Some(app_err_to_response(AppError::Internal(
"VTA DID not configured; cannot issue step-up approval".into(),
))));
}
};
let signing_key = app_try!(
operations::step_up_approval::load_vta_key0_signing_key(
&state.keys_ks,
&state.imported_ks,
&*state.seed_store,
&state.audit_ks,
&vta_did,
)
.await
);
let iat = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let approval_token = app_try!(operations::step_up_approval::build_vta_approval_token(
&vta_did,
&holder_did,
&body.rp_did,
&body.nonce,
iat,
&signing_key,
));
info!(
holder = %holder_did,
rp = %body.rp_did,
"issued VTA step-up approval token via DIDComm"
);
response(response_type, &StepUpApproveResponseBody { approval_token })
}
pub async fn handle_credential_issue(
_ctx: HandlerContext,
message: Message,
Extension(app_state): Extension<AppState>,
) -> HandlerResult {
let body: credential_exchange::IssueBody =
serde_json::from_value(message.body).map_err(handler_err)?;
let source = message.from.clone().or_else(|| message.thid.clone());
let stored = app_try!(
operations::credential_exchange::receive_issued_credential(
&app_state.vault_ks,
&body,
app_state.did_resolver.as_ref(),
source,
chrono::Utc::now(),
)
.await
);
info!(
credential_id = %stored.id,
format = ?stored.format,
from = message.from.as_deref().unwrap_or("unknown"),
"received issued credential into vault via DIDComm"
);
Ok(None)
}
pub async fn handle_credential_offer(
_ctx: HandlerContext,
message: Message,
Extension(app_state): Extension<AppState>,
) -> HandlerResult {
let body: credential_exchange::OfferBody =
serde_json::from_value(message.body).map_err(handler_err)?;
let subject_did = match app_state.config.read().await.credential_holder_did.clone() {
Some(did) => did,
None => {
info!(
from = message.from.as_deref().unwrap_or("unknown"),
"credential offer received but no credential_holder_did configured — declining"
);
return Ok(Some(DIDCommResponse::problem_report(
ProblemReport::bad_request(
"this VTA does not accept unsolicited credential offers \
(no credential_holder_did configured)"
.to_string(),
),
)));
}
};
let auth = crate::auth::AuthClaims {
role: Role::Admin,
allowed_contexts: Vec::new(),
..Default::default()
};
let request = app_try!(
operations::credential_exchange::build_credential_request_for_offer(
&app_state.keys_ks,
&app_state.seed_store,
&auth,
&body.credential_offer,
&subject_did,
chrono::Utc::now(),
)
.await
);
let request_body = serde_json::to_value(&request).map_err(handler_err)?;
info!(
from = message.from.as_deref().unwrap_or("unknown"),
subject = %subject_did,
"answered credential offer with a request"
);
Ok(Some(
DIDCommResponse::new(credential_exchange::REQUEST, request_body).thid(message.id),
))
}
pub async fn handle_credential_query(
ctx: HandlerContext,
message: Message,
Extension(app_state): Extension<AppState>,
) -> HandlerResult {
let verifier_did = ctx
.sender_did
.clone()
.ok_or_else(|| handler_err("credential query has no authcrypt sender"))?;
let body: credential_exchange::QueryBody =
serde_json::from_value(message.body).map_err(handler_err)?;
let (policy, vta_did) = {
let config = app_state.config.read().await;
(
operations::credential_exchange::ConsentPolicy::trusting(
config.trusted_presentation_verifiers.clone(),
),
config.vta_did.clone().unwrap_or_else(|| "vta:self".into()),
)
};
let auth = crate::auth::AuthClaims {
did: vta_did,
role: Role::Admin,
allowed_contexts: Vec::new(),
..Default::default()
};
let outcome = app_try!(
operations::credential_exchange::present_query(
&app_state.vault_ks,
&app_state.keys_ks,
&app_state.seed_store,
&auth,
&body,
&verifier_did,
&policy,
app_state.status_list_resolver.as_deref(),
chrono::Utc::now(),
)
.await
);
use operations::credential_exchange::PresentOutcome;
match outcome {
PresentOutcome::Presented(present_body) => {
info!(verifier = %verifier_did, "presented a vp_token via DIDComm");
response(credential_exchange::PRESENT, &present_body)
}
PresentOutcome::ConsentRequired {
requested, purpose, ..
} => {
let approval_id = message.thid.clone().unwrap_or_else(|| message.id.clone());
let requested_count = requested.len();
app_try!(
operations::credential_exchange::defer_presentation(
&app_state.vault_ks,
&approval_id,
&verifier_did,
requested,
&body,
chrono::Utc::now(),
)
.await
);
info!(
verifier = %verifier_did,
approval_id = %approval_id,
requested = requested_count,
%purpose,
"credential query deferred — holder consent required (pending approval persisted)"
);
Ok(Some(DIDCommResponse::problem_report(
ProblemReport::bad_request(format!(
"presentation requires holder consent (verifier not trusted); \
an out-of-band approval is needed (pending `{approval_id}`)"
)),
)))
}
}
}
pub async fn handle_unknown(_ctx: HandlerContext, message: Message) -> HandlerResult {
let from = message.from.as_deref().unwrap_or("unknown");
let thid = message.thid.as_deref().unwrap_or("none");
if message.typ.contains("problem-report") {
let code = message
.body
.get("code")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
let comment = message
.body
.get("comment")
.and_then(|v| v.as_str())
.unwrap_or("no details provided");
warn!(
from,
code,
comment,
thid,
msg_type = %message.typ,
"received unhandled problem-report"
);
return Ok(None);
}
warn!(from, thid, msg_type = %message.typ, "unknown message type — ignoring");
Ok(Some(DIDCommResponse::problem_report(
ProblemReport::bad_request(format!("unsupported message type: {}", message.typ)),
)))
}
#[cfg(test)]
mod tests {
use super::*;
use vta_sdk::protocols::problem_report_codes as codes;
#[test]
fn app_error_maps_to_byte_identical_codes() {
let cases = [
(AppError::Conflict("c".into()), codes::CONFLICT, "c"),
(AppError::NotFound("n".into()), codes::NOT_FOUND, "n"),
(
AppError::Authentication("a".into()),
codes::UNAUTHORIZED,
"a",
),
(AppError::Unauthorized("u".into()), codes::UNAUTHORIZED, "u"),
(AppError::Forbidden("f".into()), codes::FORBIDDEN, "f"),
(AppError::StepUpRequired("s".into()), codes::FORBIDDEN, "s"),
(AppError::Validation("v".into()), codes::BAD_REQUEST, "v"),
];
for (err, expected_code, expected_comment) in cases {
let report = app_err_to_problem_report(&err);
assert_eq!(report.code, expected_code, "code for {err:?}");
assert_eq!(report.comment, expected_comment, "comment for {err:?}");
}
}
#[test]
fn app_error_catch_all_is_internal_error() {
let report = app_err_to_problem_report(&AppError::Internal("boom".into()));
assert_eq!(report.code, codes::INTERNAL);
assert_eq!(report.comment, "internal error: boom");
}
}