use crate::crypto::SIGNATURE_ALG_ED25519;
use crate::models::{DelegationToken, RequestEnvelope, Violation};
use std::collections::HashSet;
pub const SPEC_VERSION_CURRENT: &str = "0.2";
pub const KIND_REQUEST_ENVELOPE: &str = "DelegatedRequest";
pub const KIND_DELEGATION_TOKEN: &str = "DelegatedCapability";
pub const MAX_IDENTIFIER_BYTES: usize = 512;
pub const MAX_LIST_ITEMS: usize = 64;
pub fn validate_request(envelope: &RequestEnvelope) -> Result<(), Violation> {
exact(
"request.spec_version",
&envelope.spec_version,
SPEC_VERSION_CURRENT,
)?;
exact("request.kind", &envelope.kind, KIND_REQUEST_ENVELOPE)?;
if let Some(id) = &envelope.request_id {
bounded("request.request_id", id, 1, MAX_IDENTIFIER_BYTES)?;
}
validate_token(&envelope.delegation_token)
}
pub fn validate_token(token: &DelegationToken) -> Result<(), Violation> {
exact(
"token.spec_version",
&token.spec_version,
SPEC_VERSION_CURRENT,
)?;
exact("token.kind", &token.kind, KIND_DELEGATION_TOKEN)?;
bounded("token.token_id", &token.token_id, 1, MAX_IDENTIFIER_BYTES)?;
bounded("token.issuer", &token.issuer, 1, MAX_IDENTIFIER_BYTES)?;
bounded("token.agent_id", &token.agent_id, 1, MAX_IDENTIFIER_BYTES)?;
bounded(
"token.delegator_id",
&token.delegator_id,
1,
MAX_IDENTIFIER_BYTES,
)?;
bounded("token.nonce", &token.nonce, 16, MAX_IDENTIFIER_BYTES)?;
bounded("token.key_id", &token.key_id, 1, MAX_IDENTIFIER_BYTES)?;
bounded("token.signature", &token.signature, 1, 128)?;
exact(
"token.signature_alg",
&token.signature_alg,
SIGNATURE_ALG_ED25519,
)?;
nonempty_vec("token.audience", &token.audience)?;
nonempty_vec("token.allowed_actions", &token.allowed_actions)?;
if let Some(resources) = &token.allowed_resources {
nonempty_vec("token.allowed_resources", resources)?;
}
if token.issued_at >= token.expires_at {
return Err(Violation::new(
"normalize_request",
"token.issued_at must be before token.expires_at",
));
}
Ok(())
}
fn exact(field: &str, actual: &str, expected: &str) -> Result<(), Violation> {
if actual != expected {
return Err(Violation::new(
"normalize_request",
format!("{field} must equal {expected}"),
));
}
Ok(())
}
fn nonempty_vec(field: &str, values: &[String]) -> Result<(), Violation> {
if values.is_empty() || values.len() > MAX_LIST_ITEMS {
return Err(Violation::new(
"normalize_request",
format!("{field} must contain between 1 and {MAX_LIST_ITEMS} entries"),
));
}
let mut unique = HashSet::with_capacity(values.len());
for value in values {
bounded(field, value, 1, MAX_IDENTIFIER_BYTES)?;
if !unique.insert(value) {
return Err(Violation::new(
"normalize_request",
format!("{field} must not contain duplicates"),
));
}
}
Ok(())
}
fn bounded(field: &str, value: &str, minimum: usize, maximum: usize) -> Result<(), Violation> {
let length = value.len();
if value.trim().len() != length || length < minimum || length > maximum {
return Err(Violation::new(
"normalize_request",
format!("{field} must be trimmed and contain {minimum}..={maximum} bytes"),
));
}
Ok(())
}