//! Authorization ceremonies. Build bindings from stored data and consume typed proofs. use super::{Api, Error, Result, parse, string, value}; use crate::cred::{ self, Caller, requests::Overrides, webauthn::{Binding, Decision, Gate, Proof, Verified}, }; use serde_json::{Value, json}; fn choice(body: &Value) -> Result<(Decision, Overrides)> { let decision: Decision = parse(body["decision"].clone())?; let overrides: Overrides = parse(body.get("overrides").cloned().unwrap_or(json!({})))?; if decision == Decision::Deny && overrides != Overrides::default() { return Err(Error(400, "invalid_overrides")); } Ok((decision, overrides)) } fn verify(ctx: &Api<'_>, body: &Value) -> Result { ctx.passkeys .lock() .map_err(|_| Error(500, "storage"))? .finish_assertion( string(body, "challenge_id")?, parse(body["assertion"].clone())?, ) .map_err(|_| Error(403, "assertion_failed")) } pub(super) fn field(ctx: &Api<'_>, item: &str, field: &str) -> Result { let field = ctx .cred .item_get(item)? .fields .into_iter() .find(|f| f.field_id == field) .ok_or(Error(404, "field_missing"))?; if field.public { return Err(Error(400, "secret_field_required")); } Ok(field) } pub(super) fn route( ctx: &Api<'_>, caller: &Caller, method: &str, path: &[&str], body: Value, ) -> Result { match (method, path) { ("POST", ["cred", "requests", id, "challenge"]) => { ctx.rate_limit(caller)?; let (decision, overrides) = choice(&body)?; let binding = ctx.cred.decision_binding(id, decision, overrides)?; let mut passkeys = ctx.passkeys.lock().map_err(|_| Error(500, "storage"))?; if passkeys.is_unlocked() { return Ok(json!({"unlocked":true})); } value( passkeys .start_assertion(binding) .map_err(|_| Error(403, "assertion_failed"))?, ) } ("POST", ["cred", "requests", id, "decide"]) => { let (decision, overrides, proof) = if ctx .passkeys .lock() .map_err(|_| Error(500, "storage"))? .is_unlocked() { let (decision, overrides) = choice(&body)?; (decision, overrides, Proof::from(ctx.cred.unlocked()?)) } else { let verified = verify(ctx, &body)?; let Binding::Decision { request_id, decision, overrides, .. } = verified.binding() else { return Err(Error(403, "binding_mismatch")); }; if request_id.as_str() != *id || (body.get("decision").is_some() && choice(&body)? != (*decision, overrides.clone())) { return Err(Error(403, "binding_mismatch")); } (*decision, overrides.clone(), Proof::from(verified)) }; match decision { Decision::Approve => ctx.cred.approve(id, decision, overrides, proof)?, Decision::Deny => ctx.cred.deny(id, decision, proof)?, } Ok(json!({"ok":true})) } ("GET", ["cred", "passkeys"]) => value( ctx.passkeys .lock() .map_err(|_| Error(500, "storage"))? .list() .map_err(|_| Error(500, "storage"))?, ), ("POST", ["cred", "passkeys", "register", "challenge"]) => { ctx.rate_limit(caller)?; value( ctx.passkeys .lock() .map_err(|_| Error(500, "storage"))? .start_assertion(Binding::Enrollment { label: string(&body, "label")?.into(), }) .map_err(|_| Error(403, "assertion_failed"))?, ) } ("POST", ["cred", "passkeys", "register", "start"]) => { ctx.rate_limit(caller)?; let gate: Option = body .get("gate") .filter(|v| !v.is_null()) .cloned() .map(parse) .transpose()?; value( ctx.passkeys .lock() .map_err(|_| Error(500, "storage"))? .start_registration(string(&body, "label")?.into(), gate) .map_err(|_| Error(403, "registration_failed"))?, ) } ("POST", ["cred", "passkeys", "register", "finish"]) => { ctx.passkeys .lock() .map_err(|_| Error(500, "storage"))? .finish_registration( string(&body, "challenge_id")?, parse(body["response"].clone())?, ) .map_err(|_| Error(403, "registration_failed"))?; Ok(json!({"ok":true})) } ("GET", ["cred", "entry", "key"]) => Ok(json!({"public_key":ctx.entry.public_key()})), ("POST", ["cred", "items", item, "fields", name, "challenge"]) => { ctx.rate_limit(caller)?; let field = field(ctx, item, name)?; let hash = string(&body, "ciphertext_sha256")?; if hash.len() != 64 || !hash .bytes() .all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase()) { return Err(Error(400, "invalid_hash")); } let binding = Binding::SecretEntry { item: (*item).into(), field: (*name).into(), ciphertext_sha256: hash.into(), field_type: Some(field.field_type), key_type: field.key_type, }; value( ctx.passkeys .lock() .map_err(|_| Error(500, "storage"))? .start_assertion(binding) .map_err(|_| Error(403, "assertion_failed"))?, ) } ("POST", ["cred", "items", item, "fields", name]) => { let envelope: cred::entry::Envelope = parse(body["envelope"].clone())?; let proof = verify(ctx, &body)?; let Binding::SecretEntry { item: bound_item, field: bound_field, ciphertext_sha256, field_type, key_type, } = proof.binding() else { return Err(Error(403, "binding_mismatch")); }; if bound_item.as_str() != *item || bound_field.as_str() != *name || *ciphertext_sha256 != envelope.sha256() { return Err(Error(403, "binding_mismatch")); } field(ctx, item, name)?; let input = cred::items::FieldInput { value: cred::items::FieldValue::Encrypted { key: &ctx.entry, envelope: &envelope, }, field_type: field_type.clone(), key_type: key_type.clone(), }; ctx.cred .set_field_input(item, name, &input, caller, proof)?; Ok(json!({"ok":true})) } ("POST", ["cred", "grants", id, "revoke"]) => { ctx.rate_limit(caller)?; ctx.cred.grant_status(id)?; let request = ctx.cred.file_operation_request( caller, super::request_key(&body)?, cred::edits::OperationRequest { item: None, base_version: None, change: vec![cred::edits::Operation::GrantRevoke { target: (*id).into(), }], }, )?; Ok(json!({"request":request})) } _ => Err(Error(404, "route_missing")), } }