221 lines
8.1 KiB
Rust
221 lines
8.1 KiB
Rust
//! 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<Verified> {
|
|
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<cred::items::Field> {
|
|
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<Value> {
|
|
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<Gate> = 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")),
|
|
}
|
|
}
|