use axum::Json;
use axum::extract::{Path, State};
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use vta_sdk::protocols::join_requests::JoinRequestStatusResponseBody;
use vti_common::error::AppError;
use crate::ceremony::Verdict;
use crate::join::{JoinStatus, get_join_request};
use crate::server::AppState;
pub const JOIN_STATUS_DOMAIN_TAG: &[u8] = b"vtc-join-status/v1\0";
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StatusRequestBody {
pub applicant_did: String,
pub signature: String,
}
pub async fn status(
State(state): State<AppState>,
Path(id): Path<Uuid>,
Json(body): Json<StatusRequestBody>,
) -> Result<Json<JoinRequestStatusResponseBody>, AppError> {
let resp = status_inner(&state, id, body.applicant_did, Some(&body.signature)).await?;
Ok(Json(resp))
}
pub async fn status_inner(
state: &AppState,
id: Uuid,
applicant_did: String,
signature_hex: Option<&str>,
) -> Result<JoinRequestStatusResponseBody, AppError> {
if let Some(hex_sig) = signature_hex {
verify_holder_signature(&applicant_did, id, hex_sig)?;
}
let req = get_join_request(&state.join_requests_ks, id)
.await?
.ok_or_else(|| AppError::NotFound(format!("join request not found: {id}")))?;
if req.applicant_did != applicant_did {
return Err(AppError::Validation(
"applicantDid does not match the join request applicant".into(),
));
}
let (needs, presentation_definition) = if req.status == JoinStatus::Deferred {
match req
.policy_decision
.and_then(|pd| serde_json::from_value::<Verdict>(pd).ok())
{
Some(Verdict::RequestMore(rm)) => (rm.needs, Some(rm.presentation_definition)),
_ => (Vec::new(), None),
}
} else {
(Vec::new(), None)
};
Ok(JoinRequestStatusResponseBody {
request_id: id,
status: req.status.to_string(),
needs,
presentation_definition,
})
}
fn verify_holder_signature(
applicant_did: &str,
request_id: Uuid,
signature_hex: &str,
) -> Result<(), AppError> {
let pubkey_bytes =
affinidi_crypto::did_key::did_key_to_ed25519_pub(applicant_did).map_err(|e| {
AppError::Validation(format!("applicantDid is not a parseable did:key: {e}"))
})?;
let verifying = VerifyingKey::from_bytes(&pubkey_bytes).map_err(|e| {
AppError::Validation(format!("applicantDid decodes to an invalid key: {e}"))
})?;
let payload = canonical_payload(applicant_did, request_id)?;
let signing_bytes = signing_bytes(&payload);
let raw_sig = hex::decode(signature_hex)
.map_err(|e| AppError::Validation(format!("signature is not hex: {e}")))?;
let signature = Signature::from_slice(&raw_sig).map_err(|e| {
AppError::Validation(format!("signature is not a 64-byte Ed25519 value: {e}"))
})?;
verifying
.verify(&signing_bytes, &signature)
.map_err(|e| AppError::Validation(format!("holder-binding signature failed: {e}")))?;
Ok(())
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct CanonicalPayload<'a> {
applicant_did: &'a str,
request_id: String,
}
fn canonical_payload(applicant_did: &str, request_id: Uuid) -> Result<Vec<u8>, AppError> {
serde_json::to_vec(&CanonicalPayload {
applicant_did,
request_id: request_id.to_string(),
})
.map_err(|e| AppError::Internal(format!("canonical payload serialize: {e}")))
}
fn signing_bytes(payload: &[u8]) -> Vec<u8> {
let mut buf = Vec::with_capacity(JOIN_STATUS_DOMAIN_TAG.len() + payload.len());
buf.extend_from_slice(JOIN_STATUS_DOMAIN_TAG);
buf.extend_from_slice(payload);
buf
}