api: add public credential routes and SSH signing oracle
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
//! SSH signing oracle. Rules and grants precede every key operation.
|
||||
use super::{Api, Error, Result, string};
|
||||
use crate::cred::{self, Caller, script::Script, types::FieldType};
|
||||
use base64::{Engine, engine::general_purpose::STANDARD};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
pub(super) fn handle(ctx: &Api<'_>, caller: &Caller, action: &str, body: &Value) -> Result<Value> {
|
||||
let cred = &ctx.cred;
|
||||
let host = string(body, "host")?;
|
||||
let mut items = cred.item_list()?.into_iter().filter(|item| {
|
||||
item.rules
|
||||
.iter()
|
||||
.any(|rule| cred::items::rule_matches(rule, "ssh", host, "/", "POST"))
|
||||
});
|
||||
let item = items.next().ok_or(Error(404, "item_missing"))?;
|
||||
if items.next().is_some() {
|
||||
return Err(Error(409, "ambiguous_item"));
|
||||
}
|
||||
if action == "sign" && string(body, "item")? != item.id {
|
||||
return Err(Error(403, "item_mismatch"));
|
||||
}
|
||||
let prepared = if item.free_use {
|
||||
Some((None, cred.credential(&item, true)?))
|
||||
} else {
|
||||
cred.prepare_use(&item, caller, action == "sign")?
|
||||
.map(|(grant, values)| (Some(grant), values))
|
||||
};
|
||||
let Some((grant, values)) = prepared else {
|
||||
ctx.rate_limit(caller)?;
|
||||
let request = cred.file_request(
|
||||
caller,
|
||||
&format!("ssh:{}", item.id),
|
||||
cred::requests::GrantRequest {
|
||||
items: vec![item.id.clone()],
|
||||
duration_secs: None,
|
||||
max_uses: None,
|
||||
validator: None,
|
||||
},
|
||||
)?;
|
||||
return Ok(
|
||||
json!({"error":"grant_required","message":"grant_required","item":item.id,"request":request}),
|
||||
);
|
||||
};
|
||||
let public = cred.credential(&item, false)?;
|
||||
let context = json!({"request":{"method":"POST","url":format!("ssh://{host}/"),"headers":[]},"credential":public,"item":{"id":item.id,"name":item.name},"grant":grant.as_ref().map(|g|json!({"conversation":match &g.subject {cred::types::Subject::Conversation(id)=>Some(id),cred::types::Subject::Program(_)=>None},"subject":g.subject,"uses_left":g.max_uses.map(|m|m-g.uses),"exp":g.exp}))});
|
||||
for source in [
|
||||
item.validate_js.as_deref(),
|
||||
grant.as_ref().and_then(|g| g.validator.as_deref()),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
let output = cred::v8::V8
|
||||
.evaluate(source, &context.to_string(), None, Default::default())
|
||||
.map_err(|_| Error(403, "validator_denied"))?;
|
||||
let output: Value =
|
||||
serde_json::from_str(&output).map_err(|_| Error(403, "validator_denied"))?;
|
||||
if output
|
||||
.as_bool()
|
||||
.or_else(|| output.get("allow").and_then(Value::as_bool))
|
||||
!= Some(true)
|
||||
{
|
||||
return Err(Error(403, "validator_denied"));
|
||||
}
|
||||
}
|
||||
let field = item
|
||||
.fields
|
||||
.iter()
|
||||
.find(|field| field.field_type == FieldType::SshPrivateKey)
|
||||
.ok_or(Error(400, "signing_key_missing"))?;
|
||||
let secret = values
|
||||
.get(&field.name)
|
||||
.and_then(Value::as_str)
|
||||
.ok_or(Error(400, "signing_key_missing"))?;
|
||||
let format = field.key_type.as_deref().ok_or(Error(400, "invalid_key"))?;
|
||||
if action == "identity" {
|
||||
let public_key = cred::sign::public_key(secret, None, format, "ssh")
|
||||
.map_err(|_| Error(400, "invalid_key"))?;
|
||||
Ok(
|
||||
json!({"item":item.id,"public_key":public_key,"user":public.get("user").and_then(Value::as_str)}),
|
||||
)
|
||||
} else {
|
||||
let payload = STANDARD
|
||||
.decode(string(body, "payload")?)
|
||||
.map_err(|_| Error(400, "invalid_payload"))?;
|
||||
let (padding, hash) = match format {
|
||||
"rs256" | "rsa-sha2-256" => (Some("pkcs1v15"), Some("sha256")),
|
||||
"rsa-sha2-512" => (Some("pkcs1v15"), Some("sha512")),
|
||||
_ => (None, None),
|
||||
};
|
||||
let signature = cred::sign::sign(secret, None, format, &payload, "ssh", padding, hash)
|
||||
.map_err(|_| Error(400, "sign_failed"))?;
|
||||
cred.audit_sign(&item.id, caller, &payload)?;
|
||||
Ok(json!({"signature":signature.signature}))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user