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);
}
}