use base64::{Engine as _, engine::general_purpose};
use ciborium::{de, ser};
use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};
use serde::{Deserialize, Serialize};
use crate::types::DecideRequest;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Delegation {
pub sub: String,
pub issuer: String,
pub delegated_authority: String,
pub actions: Vec<String>,
pub resource_pattern: String,
pub max_amount: Option<i64>,
pub nbf_epoch: i64,
pub exp_epoch: i64,
pub jti: String,
pub sig: Option<Vec<u8>>,
}
#[derive(Debug, Deserialize)]
pub struct IssueDelegationReq {
pub sub: String,
pub issuer: String,
pub delegated_authority: String,
pub actions: Vec<String>,
pub resource_pattern: String,
pub max_amount: Option<i64>,
pub nbf_epoch: i64,
pub exp_epoch: i64,
pub jti: String,
}
#[derive(Debug, Serialize)]
pub struct IssueDelegationResp {
pub delegation_b64: String,
pub pubkey_b64: String,
}
fn sig_message(d: &Delegation) -> Result<Vec<u8>, String> {
let mut to_sign = d.clone();
to_sign.sig = None;
let mut buf = Vec::new();
ser::into_writer(&to_sign, &mut buf).map_err(|err| err.to_string())?;
Ok(buf)
}
pub fn issue(
signing_key: &SigningKey,
req: IssueDelegationReq,
) -> Result<IssueDelegationResp, String> {
if req.sub.trim().is_empty() {
return Err("delegation subject cannot be empty".to_string());
}
if req.issuer.trim().is_empty() {
return Err("delegation issuer cannot be empty".to_string());
}
if req.actions.is_empty() {
return Err("delegation must include at least one action".to_string());
}
if req.resource_pattern.trim().is_empty() {
return Err("delegation resource_pattern cannot be empty".to_string());
}
if req.jti.trim().is_empty() {
return Err("delegation jti cannot be empty".to_string());
}
if req.exp_epoch <= req.nbf_epoch {
return Err("delegation exp_epoch must be later than nbf_epoch".to_string());
}
if let Some(max_amount) = req.max_amount {
if max_amount < 0 {
return Err("delegation max_amount cannot be negative".to_string());
}
}
let mut del = Delegation {
sub: req.sub,
issuer: req.issuer,
delegated_authority: req.delegated_authority,
actions: req.actions,
resource_pattern: req.resource_pattern,
max_amount: req.max_amount,
nbf_epoch: req.nbf_epoch,
exp_epoch: req.exp_epoch,
jti: req.jti,
sig: None,
};
let msg = sig_message(&del)?;
let sig: Signature = signing_key.sign(&msg);
del.sig = Some(sig.to_bytes().to_vec());
let mut buf = Vec::new();
ser::into_writer(&del, &mut buf).map_err(|err| err.to_string())?;
let verifying_key = signing_key.verifying_key();
Ok(IssueDelegationResp {
delegation_b64: general_purpose::STANDARD_NO_PAD.encode(&buf),
pubkey_b64: general_purpose::STANDARD_NO_PAD.encode(verifying_key.to_bytes()),
})
}
#[allow(clippy::cast_possible_wrap)]
pub fn verify(b64: &str, vkey: &VerifyingKey, req: &DecideRequest) -> Result<bool, String> {
let raw = general_purpose::STANDARD_NO_PAD
.decode(b64)
.map_err(|err| format!("delegation decode failed: {err}"))?;
let del: Delegation =
de::from_reader(raw.as_slice()).map_err(|err| format!("delegation parse failed: {err}"))?;
let msg = sig_message(&del)?;
let sig_bytes = del
.sig
.clone()
.ok_or_else(|| "delegation signature missing".to_string())?;
let sig = Signature::from_slice(&sig_bytes)
.map_err(|_| "delegation signature invalid".to_string())?;
if vkey.verify_strict(&msg, &sig).is_err() {
return Ok(false);
}
let now_epoch = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_err(|err| format!("time source invalid: {err}"))?
.as_secs() as i64;
if now_epoch < del.nbf_epoch || now_epoch > del.exp_epoch {
return Ok(false);
}
if del.sub != req.principal.id {
return Ok(false);
}
if !del.actions.contains(&req.action) {
return Ok(false);
}
let pattern = del.resource_pattern.trim_end_matches('*');
if !req.resource.id.starts_with(pattern) {
return Ok(false);
}
if let Some(max) = del.max_amount {
if let Some(ref ctx) = req.context {
if let Some(amount) = ctx.amount {
if amount > max {
return Ok(false);
}
}
}
}
Ok(true)
}