#[cfg(feature = "tee")]
use std::sync::Arc;
use crate::messaging::shim::{
DIDCommResponse, DIDCommServiceError, Extension, HandlerContext, ProblemReport,
ServiceProblemReport,
};
use affinidi_messaging_didcomm::Message;
use tracing::{info, warn};
use crate::acl::Role;
use crate::error::AppError;
use crate::operations;
use crate::server::AppState;
#[cfg(feature = "tee")]
use super::router::VtaState;
use vta_sdk::protocols::credential_exchange;
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) | AppError::Gone(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()),
AppError::InvalidCursor => ProblemReport::bad_request(e.to_string()),
_ => 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))),
}
};
}
#[cfg(feature = "tee")]
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)))
}
use trust_tasks_didcomm::ENVELOPE_TYPE as TRUST_TASK_ENVELOPE_TYPE;
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 authenticated = match message.from.as_deref() {
Some(sender) => Ok(sender),
None => Err(AppError::Authentication(
"message has no sender (from)".into(),
)),
};
let response = match authenticated {
Ok(sender) => {
crate::trust_tasks::transport::with_binding(
"didcomm",
crate::trust_tasks::accept_from_proven_sender(
&app_state,
sender,
&body,
crate::trust_tasks::transport::TransportConfidentiality::EndToEnd,
),
)
.await
}
Err(e) => {
crate::trust_tasks::sign_response(
&app_state,
crate::trust_tasks::reject_trust_task(
&body,
trust_tasks_rs::RejectReason::PermissionDenied {
reason: e.to_string(),
},
),
)
.await
}
};
let doc: serde_json::Value = serde_json::from_slice(&response.body).map_err(handler_err)?;
Ok(Some(DIDCommResponse::new(TRUST_TASK_ENVELOPE_TYPE, doc)))
}
#[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_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_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.contexts_ks,
&app_state.seed_store,
&app_state.audit_sink,
&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_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);
}
if let Some(comment) = trust_task_needs_envelope(&message.typ) {
warn!(
from,
msg_type = %message.typ,
"Trust Task arrived typed as its task URI, not in the DIDComm binding envelope — refused"
);
return Ok(Some(
DIDCommResponse::problem_report(ProblemReport::bad_request(comment))
.thid(message.id.clone()),
));
}
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
)))
.thid(message.id.clone()),
))
}
const TRUST_TASK_SPEC_PREFIX: &str = "https://trusttasks.org/spec/";
pub(crate) fn trust_task_needs_envelope(typ: &str) -> Option<String> {
typ.starts_with(TRUST_TASK_SPEC_PREFIX).then(|| {
format!(
"unsupported message type: {typ} — Trust Tasks must be carried in the DIDComm \
binding envelope `{TRUST_TASK_ENVELOPE_TYPE}` with the task document as the body"
)
})
}
#[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::Gone("g".into()), codes::CONFLICT, "g"),
(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 invalid_cursor_is_a_bad_request_not_an_internal_error() {
let report = app_err_to_problem_report(&AppError::InvalidCursor);
assert_eq!(report.code, codes::BAD_REQUEST);
assert_ne!(report.code, codes::INTERNAL);
}
#[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");
}
}