use serde::Serialize;
use serde::de::DeserializeOwned;
use serde_json::{Value, json};
use trust_tasks_rs::specs::vta::pcs as spec;
use trust_tasks_rs::{DeclaredErrorCode, RejectReason, TrustTask};
use vti_common::acl::Capability;
use crate::auth::AuthClaims;
use crate::operations::pcs::{self as engine, Caller, PcsError};
use crate::server::AppState;
use super::helpers::{
TRANSPORT_TRUST_TASK, TrustTaskOutcome, app_error_to_reject, parse_payload, reject_declared,
reject_with, require_capability, success_response,
};
fn respond<R: DeserializeOwned + Serialize>(
doc: &TrustTask<Value>,
value: Value,
) -> TrustTaskOutcome {
match serde_json::from_value::<R>(value) {
Ok(r) => success_response(doc, r),
Err(e) => reject_with(
doc,
RejectReason::InternalError {
reason: format!("{} response does not match its schema: {e}", doc.type_uri),
},
),
}
}
fn refuse(
doc: &TrustTask<Value>,
e: PcsError,
code: impl Fn(&PcsError) -> Option<DeclaredErrorCode>,
) -> TrustTaskOutcome {
match e {
PcsError::App(a) => app_error_to_reject(doc, a),
other => match code(&other) {
Some(c) => reject_declared(doc, c, other.to_string()),
None => reject_with(
doc,
RejectReason::InternalError {
reason: other.to_string(),
},
),
},
}
}
fn str_of<'a>(doc: &'a TrustTask<Value>, key: &str) -> &'a str {
doc.payload
.get(key)
.and_then(Value::as_str)
.unwrap_or_default()
}
fn value_of(doc: &TrustTask<Value>, key: &str) -> Value {
doc.payload.get(key).cloned().unwrap_or(Value::Null)
}
fn require_context(
doc: &TrustTask<Value>,
auth: &AuthClaims,
context: &str,
) -> Result<(), TrustTaskOutcome> {
auth.require_context(context)
.map_err(|e| app_error_to_reject(doc, e))
}
async fn audit(
state: &AppState,
action: &str,
auth: &AuthClaims,
resource: &str,
context: &str,
detail: Option<&str>,
) {
if let Err(e) = crate::audit::record_with_detail(
&state.audit_sink,
action,
&auth.did,
Some(resource),
"success",
Some(TRANSPORT_TRUST_TASK),
Some(context),
detail,
)
.await
{
tracing::warn!(error = %e, action, "audit record failed for a PCS engine task");
}
}
fn caller(auth: &AuthClaims) -> Caller<'_> {
Caller {
did: &auth.did,
channel: TRANSPORT_TRUST_TASK,
}
}
async fn application(
state: &AppState,
auth: &AuthClaims,
doc: &TrustTask<Value>,
not_found: DeclaredErrorCode,
) -> Result<engine::ApplicationRecord, TrustTaskOutcome> {
let id = str_of(doc, "applicationId");
engine::application_record(state, id, |ctx| auth.has_context_access(ctx))
.await
.map_err(|e| {
refuse(doc, e, |e| {
matches!(e, PcsError::NotFound).then_some(not_found)
})
})
}
pub(super) async fn handle_applications_create(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
use spec::applications::create::v0_1 as t;
if let Err(r) =
require_capability(state, auth, &doc, Capability::PcsApplicant, "pcs applicant").await
{
return r;
}
if let Err(r) = parse_payload::<t::Payload>(&doc) {
return r;
}
let context = str_of(&doc, "contextId").to_string();
if let Err(r) = require_context(&doc, auth, &context) {
return r;
}
let community = str_of(&doc, "community").to_string();
match engine::create_application(
state,
&caller(auth),
&context,
&community,
str_of(&doc, "joinDid"),
&value_of(&doc, "parameters"),
)
.await
{
Ok(app) => {
let id = app["applicationId"]
.as_str()
.unwrap_or_default()
.to_string();
audit(
state,
"pcs.application.create",
auth,
&id,
&context,
Some(&community),
)
.await;
respond::<t::Response>(&doc, json!({ "application": app }))
}
Err(e) => refuse(&doc, e, |e| match e {
PcsError::UnsupportedSuite(_) => Some(t::error_codes::UNSUPPORTED_SUITE),
PcsError::ParametersInvalid(_) => Some(t::error_codes::PARAMETERS_INVALID),
_ => None,
}),
}
}
pub(super) async fn handle_applications_show(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
use spec::applications::show::v0_1 as t;
if let Err(r) =
require_capability(state, auth, &doc, Capability::PcsApplicant, "pcs applicant").await
{
return r;
}
if let Err(r) = parse_payload::<t::Payload>(&doc) {
return r;
}
let record = match application(state, auth, &doc, t::error_codes::NOT_FOUND).await {
Ok(r) => r,
Err(r) => return r,
};
match engine::show_application(state, &record).await {
Ok(app) => respond::<t::Response>(&doc, json!({ "application": app })),
Err(e) => refuse(&doc, e, |_| None),
}
}
pub(super) async fn handle_applications_delete(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
use spec::applications::delete::v0_1 as t;
if let Err(r) =
require_capability(state, auth, &doc, Capability::PcsApplicant, "pcs applicant").await
{
return r;
}
if let Err(r) = parse_payload::<t::Payload>(&doc) {
return r;
}
let record = match application(state, auth, &doc, t::error_codes::NOT_FOUND).await {
Ok(r) => r,
Err(r) => return r,
};
match engine::delete_application(state, &record).await {
Ok(()) => {
audit(
state,
"pcs.application.delete",
auth,
&record.application_id,
&record.context_id,
None,
)
.await;
respond::<t::Response>(
&doc,
json!({ "applicationId": record.application_id, "deleted": true }),
)
}
Err(e) => refuse(&doc, e, |_| None),
}
}
pub(super) async fn handle_verify_attestation(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
use spec::verify_attestation::v0_1 as t;
if let Err(r) =
require_capability(state, auth, &doc, Capability::PcsApplicant, "pcs applicant").await
{
return r;
}
if let Err(r) = parse_payload::<t::Payload>(&doc) {
return r;
}
let record = match application(state, auth, &doc, t::error_codes::NOT_FOUND).await {
Ok(r) => r,
Err(r) => return r,
};
match engine::verify_attestation(
state,
&caller(auth),
&record,
&value_of(&doc, "parameters"),
&value_of(&doc, "attestation"),
)
.await
{
Ok((app, accepted)) => {
audit(
state,
"pcs.attestation.receive",
auth,
&record.application_id,
&record.context_id,
None,
)
.await;
respond::<t::Response>(&doc, json!({ "application": app, "accepted": accepted }))
}
Err(e) => refuse(&doc, e, |e| match e {
PcsError::UnsupportedSuite(_) => Some(t::error_codes::UNSUPPORTED_SUITE),
PcsError::ParametersInvalid(_) => Some(t::error_codes::PARAMETERS_INVALID),
PcsError::AttestationInvalid(_) => Some(t::error_codes::ATTESTATION_INVALID),
PcsError::NotFound => Some(t::error_codes::NOT_FOUND),
_ => None,
}),
}
}
pub(super) async fn handle_prove(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
use spec::prove::v0_1 as t;
if let Err(r) =
require_capability(state, auth, &doc, Capability::PcsApplicant, "pcs applicant").await
{
return r;
}
if let Err(r) = parse_payload::<t::Payload>(&doc) {
return r;
}
let record = match application(state, auth, &doc, t::error_codes::NOT_FOUND).await {
Ok(r) => r,
Err(r) => return r,
};
match engine::prove(
state,
&caller(auth),
&record,
&value_of(&doc, "parameters"),
str_of(&doc, "requirementsDigest"),
str_of(&doc, "challenge"),
)
.await
{
Ok(proof) => {
audit(
state,
"pcs.prove",
auth,
&record.application_id,
&record.context_id,
None,
)
.await;
respond::<t::Response>(&doc, json!({ "hiddenVetting": proof }))
}
Err(e) => refuse(&doc, e, |e| match e {
PcsError::UnsupportedSuite(_) => Some(t::error_codes::UNSUPPORTED_SUITE),
PcsError::ParametersInvalid(_) => Some(t::error_codes::PARAMETERS_INVALID),
PcsError::NoUsableAttestation => Some(t::error_codes::NO_USABLE_ATTESTATION),
PcsError::NotFound => Some(t::error_codes::NOT_FOUND),
_ => None,
}),
}
}
async fn vetter_preamble<P: DeserializeOwned>(
state: &AppState,
auth: &AuthClaims,
doc: &TrustTask<Value>,
) -> Result<(String, String), TrustTaskOutcome> {
require_capability(state, auth, doc, Capability::PcsVetter, "pcs vetter").await?;
parse_payload::<P>(doc)?;
let context = str_of(doc, "contextId").to_string();
require_context(doc, auth, &context)?;
Ok((context, str_of(doc, "community").to_string()))
}
pub(super) async fn handle_root_request(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
use spec::root_request::v0_1 as t;
let (context, community) = match vetter_preamble::<t::Payload>(state, auth, &doc).await {
Ok(v) => v,
Err(r) => return r,
};
let label = str_of(&doc, "label").to_string();
match engine::root_request(
state,
&caller(auth),
&context,
&community,
str_of(&doc, "memberDid"),
&value_of(&doc, "parameters"),
&label,
)
.await
{
Ok(request) => {
audit(
state,
"pcs.vetter.root_request",
auth,
&community,
&context,
Some(&label),
)
.await;
respond::<t::Response>(&doc, json!({ "request": request }))
}
Err(e) => refuse(&doc, e, |e| match e {
PcsError::UnsupportedSuite(_) => Some(t::error_codes::UNSUPPORTED_SUITE),
PcsError::ParametersInvalid(_) => Some(t::error_codes::PARAMETERS_INVALID),
PcsError::MemberMismatch => Some(t::error_codes::MEMBER_MISMATCH),
PcsError::LabelNotLive(_) => Some(t::error_codes::LABEL_NOT_LIVE),
_ => None,
}),
}
}
pub(super) async fn handle_root_accept(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
use spec::root_accept::v0_1 as t;
let (context, community) = match vetter_preamble::<t::Payload>(state, auth, &doc).await {
Ok(v) => v,
Err(r) => return r,
};
let answer = value_of(&doc, "response");
let label = answer["label"].as_str().unwrap_or_default().to_string();
match engine::root_accept(
state,
&caller(auth),
&context,
&community,
&value_of(&doc, "parameters"),
&label,
answer["preCredential"].as_str().unwrap_or_default(),
)
.await
{
Ok(vetter) => {
audit(
state,
"pcs.vetter.root_accept",
auth,
&community,
&context,
Some(&label),
)
.await;
respond::<t::Response>(&doc, json!({ "vetter": vetter }))
}
Err(e) => refuse(&doc, e, |e| match e {
PcsError::NotFound => Some(t::error_codes::NOT_FOUND),
PcsError::NoRequestInFlight => Some(t::error_codes::NO_REQUEST_IN_FLIGHT),
PcsError::CredentialInvalid(_) => Some(t::error_codes::CREDENTIAL_INVALID),
PcsError::UnsupportedSuite(_) => Some(t::error_codes::UNSUPPORTED_SUITE),
PcsError::ParametersInvalid(_) => Some(t::error_codes::PARAMETERS_INVALID),
_ => None,
}),
}
}
pub(super) async fn handle_tokens_request(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
use spec::tokens::request::v0_1 as t;
let (context, community) = match vetter_preamble::<t::Payload>(state, auth, &doc).await {
Ok(v) => v,
Err(r) => return r,
};
let label = str_of(&doc, "label").to_string();
let tick = doc.payload["tick"].as_u64().unwrap_or_default();
let Ok(tick) = u32::try_from(tick) else {
return reject_with(
&doc,
RejectReason::MalformedRequest {
reason: "tick is out of range".into(),
},
);
};
let count = doc.payload["count"].as_u64().unwrap_or_default() as usize;
match engine::tokens_request(
state,
&caller(auth),
&context,
&community,
&value_of(&doc, "parameters"),
&label,
tick,
count,
)
.await
{
Ok(request) => {
let detail = format!("{label} tick {tick}");
audit(
state,
"pcs.vetter.tokens_request",
auth,
&community,
&context,
Some(&detail),
)
.await;
respond::<t::Response>(&doc, json!({ "request": request }))
}
Err(e) => refuse(&doc, e, |e| match e {
PcsError::NotFound => Some(t::error_codes::NOT_FOUND),
PcsError::LabelNotLive(_) => Some(t::error_codes::LABEL_NOT_LIVE),
PcsError::CountExceedsDrip { .. } => Some(t::error_codes::COUNT_EXCEEDS_DRIP),
PcsError::AlreadyInFlight => Some(t::error_codes::ALREADY_IN_FLIGHT),
PcsError::UnsupportedSuite(_) => Some(t::error_codes::UNSUPPORTED_SUITE),
PcsError::ParametersInvalid(_) => Some(t::error_codes::PARAMETERS_INVALID),
_ => None,
}),
}
}
pub(super) async fn handle_tokens_accept(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
use spec::tokens::accept::v0_1 as t;
let (context, community) = match vetter_preamble::<t::Payload>(state, auth, &doc).await {
Ok(v) => v,
Err(r) => return r,
};
let answer: vti_vetting_pcs::issuer::TokenBatchWire =
match serde_json::from_value(value_of(&doc, "response")) {
Ok(a) => a,
Err(e) => {
return reject_with(
&doc,
RejectReason::MalformedRequest {
reason: format!("response: {e}"),
},
);
}
};
let detail = format!("{} tick {}", answer.label, answer.tick);
match engine::tokens_accept(
state,
&caller(auth),
&context,
&community,
&value_of(&doc, "parameters"),
answer,
)
.await
{
Ok((received, vetter)) => {
audit(
state,
"pcs.vetter.tokens_accept",
auth,
&community,
&context,
Some(&detail),
)
.await;
respond::<t::Response>(&doc, json!({ "received": received, "vetter": vetter }))
}
Err(e) => refuse(&doc, e, |e| match e {
PcsError::NotFound => Some(t::error_codes::NOT_FOUND),
PcsError::NoDrawInFlight => Some(t::error_codes::NO_DRAW_IN_FLIGHT),
PcsError::TokensInvalid(_) => Some(t::error_codes::TOKENS_INVALID),
PcsError::UnsupportedSuite(_) => Some(t::error_codes::UNSUPPORTED_SUITE),
PcsError::ParametersInvalid(_) => Some(t::error_codes::PARAMETERS_INVALID),
_ => None,
}),
}
}
pub(super) async fn handle_tokens_abandon(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
use spec::tokens::abandon::v0_1 as t;
let (context, community) = match vetter_preamble::<t::Payload>(state, auth, &doc).await {
Ok(v) => v,
Err(r) => return r,
};
let label = doc.payload["draw"]["label"]
.as_str()
.unwrap_or_default()
.to_string();
let Ok(tick) = u32::try_from(doc.payload["draw"]["tick"].as_u64().unwrap_or_default()) else {
return reject_with(
&doc,
RejectReason::MalformedRequest {
reason: "tick is out of range".into(),
},
);
};
match engine::tokens_abandon(state, &context, &community, &label, tick).await {
Ok(vetter) => {
let detail = format!("{label} tick {tick}");
audit(
state,
"pcs.vetter.tokens_abandon",
auth,
&community,
&context,
Some(&detail),
)
.await;
respond::<t::Response>(&doc, json!({ "vetter": vetter }))
}
Err(e) => refuse(&doc, e, |e| match e {
PcsError::NotFound => Some(t::error_codes::NOT_FOUND),
PcsError::NoDrawInFlight => Some(t::error_codes::NO_DRAW_IN_FLIGHT),
_ => None,
}),
}
}
pub(super) async fn handle_attest(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
use spec::attest::v0_1 as t;
let (context, community) = match vetter_preamble::<t::Payload>(state, auth, &doc).await {
Ok(v) => v,
Err(r) => return r,
};
let mut meta = value_of(&doc, "meta");
if let Some(m) = meta.as_object_mut() {
m.insert("tokenLabel".into(), Value::String(String::new()));
m.insert("tokenSerial".into(), Value::String(String::new()));
}
let meta = match serde_json::from_value(meta) {
Ok(m) => m,
Err(e) => {
return reject_with(
&doc,
RejectReason::MalformedRequest {
reason: format!("meta: {e}"),
},
);
}
};
let prefer = doc.payload.get("preferLabel").and_then(Value::as_str);
match engine::attest(
state,
&caller(auth),
&context,
&community,
&value_of(&doc, "parameters"),
str_of(&doc, "applicantId"),
meta,
prefer,
)
.await
{
Ok(attestation) => {
let label = attestation["token"]["label"]
.as_str()
.unwrap_or_default()
.to_string();
audit(
state,
"pcs.vetter.attest",
auth,
&community,
&context,
Some(&label),
)
.await;
respond::<t::Response>(&doc, json!({ "attestation": attestation }))
}
Err(e) => refuse(&doc, e, |e| match e {
PcsError::NotFound => Some(t::error_codes::NOT_FOUND),
PcsError::NotEnrolled => Some(t::error_codes::NOT_ENROLLED),
PcsError::NoToken => Some(t::error_codes::NO_TOKEN),
PcsError::ApplicantInvalid => Some(t::error_codes::APPLICANT_INVALID),
PcsError::CommunityMismatch => Some(t::error_codes::COMMUNITY_MISMATCH),
PcsError::UnsupportedSuite(_) => Some(t::error_codes::UNSUPPORTED_SUITE),
PcsError::ParametersInvalid(_) => Some(t::error_codes::PARAMETERS_INVALID),
_ => None,
}),
}
}
pub(super) async fn handle_vetter_show(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
use spec::vetter::show::v0_1 as t;
let (context, community) = match vetter_preamble::<t::Payload>(state, auth, &doc).await {
Ok(v) => v,
Err(r) => return r,
};
match engine::vetter_show(state, &context, &community).await {
Ok(vetter) => respond::<t::Response>(&doc, json!({ "vetter": vetter })),
Err(e) => refuse(&doc, e, |e| {
matches!(e, PcsError::NotFound).then_some(t::error_codes::NOT_FOUND)
}),
}
}
#[cfg(test)]
#[path = "pcs_tests.rs"]
mod tests;