use crate::types::{AuthorityCapability, AuthorityDerivationToken, EgressRule, ExecutionCellSpec};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AuthorityValidationResult {
Valid,
InvalidSignature {
reason: String,
},
ExceedsGrantorEgress {
declared: usize,
claimed: usize,
},
ExceedsGrantorSecrets {
declared: usize,
claimed: usize,
},
MissingDerivationToken,
ExpiredToken {
not_before: String,
not_after: String,
},
}
impl AuthorityValidationResult {
pub fn is_valid(&self) -> bool {
matches!(self, Self::Valid)
}
}
pub fn validate_authority_derivation(
spec: &ExecutionCellSpec,
token: &AuthorityDerivationToken,
role_keys: &std::collections::HashMap<String, String>,
) -> AuthorityValidationResult {
let spec_capability = AuthorityCapability {
egress_rules: spec.authority.egress_rules.clone().unwrap_or_default(),
secret_refs: spec.authority.secret_refs.clone().unwrap_or_default(),
};
if !egress_subset(
&token.leaf_capability.egress_rules,
&spec_capability.egress_rules,
) {
return AuthorityValidationResult::ExceedsGrantorEgress {
declared: spec_capability.egress_rules.len(),
claimed: token.leaf_capability.egress_rules.len(),
};
}
if !secret_subset(
&token.leaf_capability.secret_refs,
&spec_capability.secret_refs,
) {
return AuthorityValidationResult::ExceedsGrantorSecrets {
declared: spec_capability.secret_refs.len(),
claimed: token.leaf_capability.secret_refs.len(),
};
}
match crate::verify_authority_derivation(spec, token, role_keys) {
Ok(()) => AuthorityValidationResult::Valid,
Err(e) => AuthorityValidationResult::InvalidSignature {
reason: e.to_string(),
},
}
}
fn egress_subset(child: &[EgressRule], parent: &[EgressRule]) -> bool {
child.iter().all(|cr| {
parent.iter().any(|pr| {
pr.host.eq_ignore_ascii_case(&cr.host)
&& pr.port == cr.port
&& pr.protocol == cr.protocol
})
})
}
fn secret_subset(child: &[String], parent: &[String]) -> bool {
child.iter().all(|cs| parent.iter().any(|ps| ps == cs))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{AuthorityBundle, AuthoritySignature, EgressRule, Lifetime, RoleId};
fn egress(host: &str, port: u16, proto: Option<&str>) -> EgressRule {
EgressRule {
host: host.to_string(),
port,
protocol: proto.map(str::to_string),
dns_egress_justification: None,
}
}
fn spec_with(egress_rules: Vec<EgressRule>, secret_refs: Vec<String>) -> ExecutionCellSpec {
ExecutionCellSpec {
id: "deriv-test".into(),
correlation: None,
ingress: None,
environment: None,
placement: None,
policy: None,
identity: None,
run: None,
authority: AuthorityBundle {
filesystem: None,
network: None,
egress_rules: Some(egress_rules),
secret_refs: Some(secret_refs),
authority_derivation: None,
dns_authority: None,
cdn_authority: None,
},
lifetime: Lifetime { ttl_seconds: 60 },
export: None,
telemetry: None,
}
}
fn token_with(leaf: AuthorityCapability) -> AuthorityDerivationToken {
AuthorityDerivationToken {
role_root: RoleId("test-role".into()),
parent_run_id: Some("run-0".into()),
derivation_steps: vec![],
leaf_capability: leaf,
grantor_signature: AuthoritySignature {
algorithm: "ed25519".into(),
bytes: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".into(),
},
}
}
#[test]
fn is_valid_helper_matches_variant() {
assert!(AuthorityValidationResult::Valid.is_valid());
assert!(!AuthorityValidationResult::MissingDerivationToken.is_valid());
assert!(!AuthorityValidationResult::ExceedsGrantorEgress {
declared: 2,
claimed: 3,
}
.is_valid());
}
#[test]
fn exceeds_egress_when_leaf_claims_rule_spec_does_not_declare() {
let spec = spec_with(vec![egress("api.example.com", 443, Some("tcp"))], vec![]);
let token = token_with(AuthorityCapability {
egress_rules: vec![egress("evil.example.com", 443, Some("tcp"))],
secret_refs: vec![],
});
let role_keys = std::collections::HashMap::new();
let result = validate_authority_derivation(&spec, &token, &role_keys);
assert_eq!(
result,
AuthorityValidationResult::ExceedsGrantorEgress {
declared: 1,
claimed: 1,
}
);
}
#[test]
fn exceeds_secrets_when_leaf_claims_ref_spec_does_not_declare() {
let spec = spec_with(vec![], vec!["api-key".into()]);
let token = token_with(AuthorityCapability {
egress_rules: vec![],
secret_refs: vec!["root-token".into()],
});
let role_keys = std::collections::HashMap::new();
let result = validate_authority_derivation(&spec, &token, &role_keys);
assert_eq!(
result,
AuthorityValidationResult::ExceedsGrantorSecrets {
declared: 1,
claimed: 1,
}
);
}
#[test]
fn leaf_narrower_than_spec_is_structurally_valid() {
let spec = spec_with(
vec![
egress("api.example.com", 443, Some("tcp")),
egress("db.example.com", 5432, Some("tcp")),
],
vec!["k1".into(), "k2".into()],
);
let token = token_with(AuthorityCapability {
egress_rules: vec![egress("api.example.com", 443, Some("tcp"))],
secret_refs: vec!["k1".into()],
});
let role_keys = std::collections::HashMap::new();
let result = validate_authority_derivation(&spec, &token, &role_keys);
assert!(
matches!(result, AuthorityValidationResult::InvalidSignature { .. }),
"expected InvalidSignature (structural narrowing OK), got {result:?}"
);
}
#[test]
fn structural_subset_then_signature_failure_collapses_to_invalid_signature() {
let rule = egress("api.example.com", 443, Some("tcp"));
let spec = spec_with(vec![rule.clone()], vec!["k".into()]);
let token = token_with(AuthorityCapability {
egress_rules: vec![rule],
secret_refs: vec!["k".into()],
});
let role_keys = std::collections::HashMap::new();
let result = validate_authority_derivation(&spec, &token, &role_keys);
match result {
AuthorityValidationResult::InvalidSignature { reason } => {
assert!(!reason.is_empty(), "reason should carry diagnostic");
}
other => panic!("expected InvalidSignature, got {other:?}"),
}
}
#[test]
fn missing_derivation_token_variant_is_inert() {
let v = AuthorityValidationResult::MissingDerivationToken;
assert!(!v.is_valid());
}
#[test]
fn expired_token_variant_preserves_window() {
let v = AuthorityValidationResult::ExpiredToken {
not_before: "2026-01-01T00:00:00Z".into(),
not_after: "2026-02-01T00:00:00Z".into(),
};
match v {
AuthorityValidationResult::ExpiredToken {
not_before,
not_after,
} => {
assert!(not_before.contains("2026-01-01"));
assert!(not_after.contains("2026-02-01"));
}
_ => panic!("variant mismatch"),
}
}
#[test]
fn host_match_is_case_insensitive_for_egress_subset() {
let spec = spec_with(vec![egress("API.example.com", 443, Some("tcp"))], vec![]);
let token = token_with(AuthorityCapability {
egress_rules: vec![egress("api.example.com", 443, Some("tcp"))],
secret_refs: vec![],
});
let role_keys = std::collections::HashMap::new();
let result = validate_authority_derivation(&spec, &token, &role_keys);
assert!(matches!(
result,
AuthorityValidationResult::InvalidSignature { .. }
));
}
}