delegated 0.2.2

Minimal fail-closed capability-token evaluation core
Documentation
use base64ct::Encoding;
use chrono::{DateTime, Utc};
use delegated::{
    Evaluator, InMemoryTrustState, OperationContext, PinnedIssuerKeys, TrustStateAdmin,
    TrustStateStore,
};
use ed25519_dalek::{SigningKey, VerifyingKey};
use serde::Deserialize;
use serde_json::Value;
use std::fs;
use std::path::PathBuf;

#[derive(Debug, Deserialize)]
struct Manifest {
    evaluate_at: DateTime<Utc>,
    signing_key: SigningKeyInfo,
    trusted_issuer: String,
    trusted_key_id: String,
    vectors: Vec<Vector>,
}

#[derive(Debug, Deserialize)]
struct SigningKeyInfo {
    seed_hex: String,
    public_key_b64url: String,
}

#[derive(Debug, Deserialize)]
struct Vector {
    id: String,
    #[allow(dead_code)]
    description: String,
    operation: OperationSpec,
    trust_state: Option<TrustStateSpec>,
    steps: Option<Vec<StepSpec>>,
    envelope: Option<Value>,
    raw_request: Option<String>,
    expected: Option<ExpectedSpec>,
}

#[derive(Debug, Deserialize, Clone)]
struct OperationSpec {
    audience: String,
    action: String,
    resource: Option<String>,
    delegation_depth: Option<u16>,
}

#[derive(Debug, Deserialize, Default)]
struct TrustStateSpec {
    #[serde(default)]
    revoke_tokens: Vec<[String; 2]>,
    #[serde(default)]
    deny_agents: Vec<[String; 2]>,
    #[serde(default)]
    preconsume_nonces: Vec<[String; 2]>,
}

#[derive(Debug, Deserialize)]
struct StepSpec {
    envelope: Value,
    expected: ExpectedSpec,
}

#[derive(Debug, Deserialize, Clone)]
struct ExpectedSpec {
    allowed: bool,
    stage: String,
    reason_contains: Option<String>,
}

fn fixture_path() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/vectors.json")
}

fn load_manifest() -> Manifest {
    let raw = fs::read_to_string(fixture_path()).expect("vectors fixture must exist");
    serde_json::from_str(&raw).expect("vectors fixture must parse")
}

fn signing_key(manifest: &Manifest) -> SigningKey {
    let seed = hex::decode(manifest.signing_key.seed_hex.trim()).expect("seed_hex must decode");
    let bytes: [u8; 32] = seed.try_into().expect("seed must be 32 bytes");
    SigningKey::from_bytes(&bytes)
}

fn verifying_key(manifest: &Manifest) -> VerifyingKey {
    signing_key(manifest).verifying_key()
}

fn operation(spec: &OperationSpec) -> OperationContext {
    let mut context = OperationContext::new(&spec.audience, &spec.action);
    if let Some(resource) = &spec.resource {
        context = context.with_resource(resource.clone());
    }
    if let Some(depth) = spec.delegation_depth {
        context = context.with_delegation_depth(depth);
    }
    context
}

fn apply_trust_state(state: &InMemoryTrustState, setup: &TrustStateSpec, now: DateTime<Utc>) {
    for [issuer, token_id] in &setup.revoke_tokens {
        state.revoke_token(issuer, token_id).unwrap();
    }
    for [issuer, agent_id] in &setup.deny_agents {
        state.deny_agent(issuer, agent_id).unwrap();
    }
    for [issuer, nonce] in &setup.preconsume_nonces {
        state
            .consume_nonce(issuer, nonce, now, now + chrono::Duration::hours(1))
            .unwrap();
    }
}

fn pinned_keys(
    manifest: &Manifest,
    vector_id: &str,
    verifying_key: VerifyingKey,
) -> PinnedIssuerKeys {
    let keys = PinnedIssuerKeys::new();
    if vector_id != "deny-untrusted-issuer" {
        keys.insert(
            &manifest.trusted_issuer,
            &manifest.trusted_key_id,
            verifying_key,
        )
        .unwrap();
    }
    keys
}

fn request_bytes(vector: &Vector, step: Option<&StepSpec>) -> Vec<u8> {
    if let Some(raw) = &vector.raw_request {
        return raw.as_bytes().to_vec();
    }
    let envelope = step
        .map(|step| &step.envelope)
        .or(vector.envelope.as_ref())
        .expect("vector must provide envelope or raw_request");
    serde_json::to_vec(envelope).unwrap()
}

fn assert_expected(actual: &delegated::Decision, expected: &ExpectedSpec, vector_id: &str) {
    assert_eq!(
        actual.allowed, expected.allowed,
        "vector {vector_id}: allowed mismatch"
    );
    assert_eq!(
        actual.stage, expected.stage,
        "vector {vector_id}: stage mismatch"
    );
    if let Some(fragment) = &expected.reason_contains {
        assert!(
            actual.reason.contains(fragment),
            "vector {vector_id}: expected reason containing `{fragment}`, got `{}`",
            actual.reason
        );
    }
}

#[test]
fn reference_vectors_match_rust_evaluator() {
    let manifest = load_manifest();
    let verifying_key = verifying_key(&manifest);
    assert_eq!(
        manifest.signing_key.public_key_b64url,
        base64ct::Base64UrlUnpadded::encode_string(verifying_key.as_bytes())
    );

    for vector in &manifest.vectors {
        let keys = pinned_keys(&manifest, &vector.id, verifying_key);
        let state = InMemoryTrustState::new();
        if let Some(setup) = &vector.trust_state {
            apply_trust_state(&state, setup, manifest.evaluate_at);
        }
        let evaluator = Evaluator::new(&keys, &state);
        let operation = operation(&vector.operation);

        if let Some(steps) = &vector.steps {
            for (index, step) in steps.iter().enumerate() {
                let (decision, _) = evaluator.evaluate(
                    &request_bytes(vector, Some(step)),
                    &operation,
                    manifest.evaluate_at,
                );
                assert_expected(
                    &decision,
                    &step.expected,
                    &format!("{}[step {index}]", vector.id),
                );
            }
            continue;
        }

        let expected = vector
            .expected
            .as_ref()
            .expect("single-shot vector must include expected");
        let (decision, _) = evaluator.evaluate(
            &request_bytes(vector, None),
            &operation,
            manifest.evaluate_at,
        );
        assert_expected(&decision, expected, &vector.id);
    }
}