delegated 0.2.2

Minimal fail-closed capability-token evaluation core
Documentation
use chrono::{Duration, TimeZone, Utc};
use delegated::{
    AuditSink, DelegationTokenBuilder, Evaluator, InMemoryTrustState, OperationContext,
    PinnedIssuerKeys, TrustStateAdmin, TrustStateError, TrustStateStore, VecAuditSink, envelope,
};
use ed25519_dalek::SigningKey;
use serde_json::{Value, json};

fn now() -> chrono::DateTime<Utc> {
    Utc.with_ymd_and_hms(2026, 7, 1, 12, 0, 0).single().unwrap()
}

fn fixture() -> (Value, SigningKey, PinnedIssuerKeys) {
    let key = SigningKey::from_bytes(&[7; 32]);
    let keys = PinnedIssuerKeys::new();
    keys.insert("https://issuer.example", "issuer-1", key.verifying_key())
        .unwrap();
    let request = signed_request(
        &key,
        "https://issuer.example",
        "issuer-1",
        "nonce-0000000001",
        now() - Duration::minutes(1),
        now() + Duration::minutes(10),
    );
    (request, key, keys)
}

fn signed_request(
    key: &SigningKey,
    issuer: &str,
    key_id: &str,
    nonce: &str,
    issued_at: chrono::DateTime<Utc>,
    expires_at: chrono::DateTime<Utc>,
) -> Value {
    let token = DelegationTokenBuilder::new()
        .token_id("token-1")
        .issuer(issuer)
        .agent_id("agent:scheduler")
        .delegator_id("user:alice")
        .audience("calendar-api")
        .allowed_action("calendar.create")
        .allowed_resource("calendar:alice")
        .max_delegation_depth(0)
        .issued_at(issued_at)
        .expires_at(expires_at)
        .nonce(nonce)
        .key_id(key_id)
        .build_and_sign(key)
        .unwrap();
    serde_json::to_value(envelope(token, Some("request-1".into()))).unwrap()
}

fn operation() -> OperationContext {
    OperationContext::new("calendar-api", "calendar.create")
        .with_resource("calendar:alice")
        .with_delegation_depth(0)
}

fn encode(value: &Value) -> Vec<u8> {
    serde_json::to_vec(value).unwrap()
}

#[test]
fn allows_a_trusted_issuer_for_the_actual_operation() {
    let (request, _, keys) = fixture();
    let state = InMemoryTrustState::new();
    let (decision, event) =
        Evaluator::new(&keys, &state).evaluate(&encode(&request), &operation(), now());
    assert!(decision.allowed);
    assert_eq!(event.action, "calendar.create");
    assert_eq!(event.resource.as_deref(), Some("calendar:alice"));
}

#[test]
fn rejects_an_untrusted_self_selected_issuer_key() {
    let (request, _, _) = fixture();
    let attacker_keys = PinnedIssuerKeys::new();
    let state = InMemoryTrustState::new();
    let (decision, _) =
        Evaluator::new(&attacker_keys, &state).evaluate(&encode(&request), &operation(), now());
    assert!(!decision.allowed);
    assert_eq!(decision.stage, "verify_issuer");
}

#[test]
fn rejects_tampering_with_signed_claims() {
    let (mut request, _, keys) = fixture();
    request["delegation_token"]["agent_id"] = json!("agent:attacker");
    let state = InMemoryTrustState::new();
    let (decision, _) =
        Evaluator::new(&keys, &state).evaluate(&encode(&request), &operation(), now());
    assert!(!decision.allowed);
    assert_eq!(decision.stage, "verify_issuer");
}

#[test]
fn host_operation_cannot_be_overridden_by_request_fields() {
    let (mut request, _, keys) = fixture();
    request["action"] = json!("calendar.create");
    let state = InMemoryTrustState::new();
    let operation = OperationContext::new("calendar-api", "calendar.delete")
        .with_resource("calendar:alice")
        .with_delegation_depth(0);
    let (decision, _) =
        Evaluator::new(&keys, &state).evaluate(&encode(&request), &operation, now());
    assert!(!decision.allowed);
    assert_eq!(decision.stage, "normalize_request");
}

#[test]
fn constrained_resource_and_depth_are_required() {
    let (request, _, keys) = fixture();
    let state = InMemoryTrustState::new();
    let missing_resource =
        OperationContext::new("calendar-api", "calendar.create").with_delegation_depth(0);
    let (decision, _) =
        Evaluator::new(&keys, &state).evaluate(&encode(&request), &missing_resource, now());
    assert!(!decision.allowed);
    assert!(decision.reason.contains("resource is required"));

    let missing_depth =
        OperationContext::new("calendar-api", "calendar.create").with_resource("calendar:alice");
    let (decision, _) =
        Evaluator::new(&keys, &state).evaluate(&encode(&request), &missing_depth, now());
    assert!(!decision.allowed);
    assert!(decision.reason.contains("depth is required"));

    let wrong_resource = OperationContext::new("calendar-api", "calendar.create")
        .with_resource("calendar:bob")
        .with_delegation_depth(0);
    let (decision, _) =
        Evaluator::new(&keys, &state).evaluate(&encode(&request), &wrong_resource, now());
    assert!(!decision.allowed);
    assert!(decision.reason.contains("resource is not authorized"));

    let excessive_depth = OperationContext::new("calendar-api", "calendar.create")
        .with_resource("calendar:alice")
        .with_delegation_depth(1);
    let (decision, _) =
        Evaluator::new(&keys, &state).evaluate(&encode(&request), &excessive_depth, now());
    assert!(!decision.allowed);
    assert!(decision.reason.contains("depth exceeds"));
}

#[test]
fn rejects_invalid_lifetime_windows() {
    let key = SigningKey::from_bytes(&[7; 32]);
    let keys = PinnedIssuerKeys::new();
    keys.insert("https://issuer.example", "issuer-1", key.verifying_key())
        .unwrap();
    let state = InMemoryTrustState::new();
    let evaluator = Evaluator::new(&keys, &state);

    let cases = [
        signed_request(
            &key,
            "https://issuer.example",
            "issuer-1",
            "nonce-expired-0001",
            now() - Duration::hours(2),
            now() - Duration::hours(1),
        ),
        signed_request(
            &key,
            "https://issuer.example",
            "issuer-1",
            "nonce-future-00001",
            now() + Duration::hours(1),
            now() + Duration::hours(2),
        ),
        signed_request(
            &key,
            "https://issuer.example",
            "issuer-1",
            "nonce-too-long-001",
            now() - Duration::minutes(1),
            now() + Duration::hours(25),
        ),
    ];
    for request in cases {
        let decision = evaluator.evaluate(&encode(&request), &operation(), now()).0;
        assert!(!decision.allowed);
        assert_eq!(decision.stage, "validate_lifetime");
    }
}

#[test]
fn rejects_oversized_envelopes_before_parsing() {
    let (_, _, keys) = fixture();
    let state = InMemoryTrustState::new();
    let input = vec![b' '; delegated::DEFAULT_MAX_ENVELOPE_BYTES + 1];
    let (decision, event) = Evaluator::new(&keys, &state).evaluate(&input, &operation(), now());
    assert!(!decision.allowed);
    assert_eq!(decision.stage, "normalize_request");
    assert!(!event.claims_verified);
}

#[test]
fn nonce_consumption_is_atomic_under_concurrency() {
    use std::sync::{Arc, Barrier};

    let (request, _, keys) = fixture();
    let request = Arc::new(encode(&request));
    let keys = Arc::new(keys);
    let state = Arc::new(InMemoryTrustState::new());
    let barrier = Arc::new(Barrier::new(16));
    let mut workers = Vec::new();
    for _ in 0..16 {
        let request = Arc::clone(&request);
        let keys = Arc::clone(&keys);
        let state = Arc::clone(&state);
        let barrier = Arc::clone(&barrier);
        workers.push(std::thread::spawn(move || {
            barrier.wait();
            Evaluator::new(keys.as_ref(), state.as_ref())
                .evaluate(request.as_ref(), &operation(), now())
                .0
                .allowed
        }));
    }
    let allowed = workers
        .into_iter()
        .map(|worker| worker.join().unwrap())
        .filter(|allowed| *allowed)
        .count();
    assert_eq!(allowed, 1);
}

#[test]
fn unauthorized_attempt_does_not_burn_nonce() {
    let (request, _, keys) = fixture();
    let state = InMemoryTrustState::new();
    let wrong = OperationContext::new("calendar-api", "calendar.delete")
        .with_resource("calendar:alice")
        .with_delegation_depth(0);
    let evaluator = Evaluator::new(&keys, &state);
    assert!(
        !evaluator
            .evaluate(&encode(&request), &wrong, now())
            .0
            .allowed
    );
    assert!(
        evaluator
            .evaluate(&encode(&request), &operation(), now())
            .0
            .allowed
    );
    let replay = evaluator.evaluate(&encode(&request), &operation(), now()).0;
    assert!(!replay.allowed);
    assert_eq!(replay.stage, "replay");
}

#[test]
fn revocation_and_backend_errors_fail_closed() {
    let (request, _, keys) = fixture();
    let state = InMemoryTrustState::new();
    state
        .revoke_token("https://issuer.example", "token-1")
        .unwrap();
    let (decision, _) =
        Evaluator::new(&keys, &state).evaluate(&encode(&request), &operation(), now());
    assert!(!decision.allowed);
    assert_eq!(decision.stage, "revocation");

    struct FailedStore;
    impl TrustStateStore for FailedStore {
        fn is_token_revoked(&self, _: &str, _: &str) -> Result<bool, TrustStateError> {
            Err(TrustStateError::new("offline"))
        }
        fn is_agent_denied(&self, _: &str, _: &str) -> Result<bool, TrustStateError> {
            Err(TrustStateError::new("offline"))
        }
        fn consume_nonce(
            &self,
            _: &str,
            _: &str,
            _: chrono::DateTime<Utc>,
            _: chrono::DateTime<Utc>,
        ) -> Result<bool, TrustStateError> {
            Err(TrustStateError::new("offline"))
        }
    }
    let (decision, _) =
        Evaluator::new(&keys, &FailedStore).evaluate(&encode(&request), &operation(), now());
    assert!(!decision.allowed);
    assert_eq!(decision.stage, "trust_state");
}

#[test]
fn audit_is_mandatory_in_the_combined_api() {
    let (request, _, keys) = fixture();
    let state = InMemoryTrustState::new();
    let sink = VecAuditSink::new();
    let decision = Evaluator::new(&keys, &state)
        .evaluate_and_audit(&encode(&request), &operation(), now(), &sink)
        .unwrap();
    assert!(decision.allowed);
    assert_eq!(sink.events().unwrap().len(), 1);

    struct FailedAudit;
    impl AuditSink for FailedAudit {
        fn write_event(&self, _: &delegated::AuditEvent) -> std::io::Result<()> {
            Err(std::io::Error::other("offline"))
        }
    }
    let (second_request, _, second_keys) = fixture();
    let second_state = InMemoryTrustState::new();
    let result = Evaluator::new(&second_keys, &second_state).evaluate_and_audit(
        &encode(&second_request),
        &operation(),
        now(),
        &FailedAudit,
    );
    assert!(result.is_err());
}

#[test]
fn mutable_state_keys_are_scoped_by_issuer() {
    let key_a = SigningKey::from_bytes(&[7; 32]);
    let key_b = SigningKey::from_bytes(&[8; 32]);
    let keys = PinnedIssuerKeys::new();
    keys.insert("issuer-a", "key", key_a.verifying_key())
        .unwrap();
    keys.insert("issuer-b", "key", key_b.verifying_key())
        .unwrap();
    let request_a = signed_request(
        &key_a,
        "issuer-a",
        "key",
        "shared-nonce-0001",
        now() - Duration::minutes(1),
        now() + Duration::minutes(10),
    );
    let request_b = signed_request(
        &key_b,
        "issuer-b",
        "key",
        "shared-nonce-0001",
        now() - Duration::minutes(1),
        now() + Duration::minutes(10),
    );
    let state = InMemoryTrustState::new();
    state.revoke_token("issuer-a", "token-1").unwrap();
    let evaluator = Evaluator::new(&keys, &state);
    assert!(
        !evaluator
            .evaluate(&encode(&request_a), &operation(), now())
            .0
            .allowed
    );
    assert!(
        evaluator
            .evaluate(&encode(&request_b), &operation(), now())
            .0
            .allowed
    );
}