delegated 0.2.2

Minimal fail-closed capability-token evaluation core
Documentation
//! Wire-contract constants and structural validation.

use crate::crypto::SIGNATURE_ALG_ED25519;
use crate::models::{DelegationToken, RequestEnvelope, Violation};
use std::collections::HashSet;

/// Current wire-format version accepted by this crate.
pub const SPEC_VERSION_CURRENT: &str = "0.2";
/// Required `kind` value for request envelopes.
pub const KIND_REQUEST_ENVELOPE: &str = "DelegatedRequest";
/// Required `kind` value for signed capability tokens.
pub const KIND_DELEGATION_TOKEN: &str = "DelegatedCapability";
/// Maximum UTF-8 byte length accepted for identifiers and authorization values.
pub const MAX_IDENTIFIER_BYTES: usize = 512;
/// Maximum number of entries accepted in an authorization list.
pub const MAX_LIST_ITEMS: usize = 64;

/// Validates a decoded request envelope and its nested capability token.
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)
}

/// Validates the structural contract of a capability token without verifying its signature.
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(())
}