1use crate::cert::DelegationCert;
2use crate::chain::DyoloChain;
3use crate::error::A1Error;
4use crate::intent::Intent;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
9#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
10pub enum PolicyViolation {
11 ChainDepthExceeded { allowed: u8, actual: usize },
12 IntentNotAllowed { intent: String, policy: String },
13 TtlExceedsMaximum { allowed_secs: u64, actual_secs: u64 },
14 SubDelegationForbidden,
15 RequiredExtensionAbsent { key: String },
16 PrincipalNotTrusted,
17}
18
19impl std::fmt::Display for PolicyViolation {
20 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21 match self {
22 Self::ChainDepthExceeded { allowed, actual } => {
23 write!(f, "chain depth {actual} exceeds policy maximum {allowed}")
24 }
25 Self::IntentNotAllowed { intent, policy } => {
26 write!(f, "intent '{intent}' not permitted under policy '{policy}'")
27 }
28 Self::TtlExceedsMaximum {
29 allowed_secs,
30 actual_secs,
31 } => write!(
32 f,
33 "cert TTL {actual_secs}s exceeds policy maximum {allowed_secs}s"
34 ),
35 Self::SubDelegationForbidden => {
36 write!(f, "sub-delegation is not permitted under this policy")
37 }
38 Self::RequiredExtensionAbsent { key } => {
39 write!(f, "required extension '{key}' is absent from cert")
40 }
41 Self::PrincipalNotTrusted => {
42 write!(f, "principal public key is not in the trusted set")
43 }
44 }
45 }
46}
47
48impl std::error::Error for PolicyViolation {}
49
50#[derive(Debug, Clone, Default)]
71#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
72pub struct CapabilitySet {
73 prefixes: Vec<String>,
74}
75
76impl CapabilitySet {
77 pub fn new() -> Self {
78 Self::default()
79 }
80
81 pub fn wildcard() -> Self {
82 Self {
83 prefixes: vec!["*".to_owned()],
84 }
85 }
86
87 pub fn allow(mut self, prefix: impl Into<String>) -> Self {
88 self.prefixes.push(prefix.into());
89 self
90 }
91
92 pub fn permits(&self, action: &str) -> bool {
93 self.prefixes.iter().any(|p| {
94 if p == "*" {
95 return true;
96 }
97 if p.ends_with('.') || p.ends_with('*') {
98 let stem = p.trim_end_matches(['*', '.']);
99 action.starts_with(stem)
100 } else {
101 action == p.as_str()
102 }
103 })
104 }
105
106 pub fn is_empty(&self) -> bool {
107 self.prefixes.is_empty()
108 }
109}
110
111#[derive(Debug, Clone)]
138#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
139pub struct DelegationPolicy {
140 name: String,
141 max_chain_depth: Option<u8>,
142 max_ttl_secs: Option<u64>,
143 capabilities: Option<CapabilitySet>,
144 allow_sub_delegation: bool,
145 required_extensions: Vec<String>,
146 trusted_principal_pks: Vec<[u8; 32]>,
147}
148
149impl DelegationPolicy {
150 pub fn new(name: impl Into<String>) -> Self {
151 Self {
152 name: name.into(),
153 max_chain_depth: None,
154 max_ttl_secs: None,
155 capabilities: None,
156 allow_sub_delegation: true,
157 required_extensions: Vec::new(),
158 trusted_principal_pks: Vec::new(),
159 }
160 }
161
162 pub fn permissive() -> Self {
163 Self::new("permissive")
164 }
165
166 pub fn max_chain_depth(mut self, depth: u8) -> Self {
167 self.max_chain_depth = Some(depth);
168 self
169 }
170
171 pub fn max_ttl_secs(mut self, secs: u64) -> Self {
172 self.max_ttl_secs = Some(secs);
173 self
174 }
175
176 pub fn capabilities(mut self, caps: CapabilitySet) -> Self {
177 self.capabilities = Some(caps);
178 self
179 }
180
181 pub fn forbid_sub_delegation(mut self) -> Self {
182 self.allow_sub_delegation = false;
183 self
184 }
185
186 pub fn require_extension(mut self, key: impl Into<String>) -> Self {
187 self.required_extensions.push(key.into());
188 self
189 }
190
191 pub fn trust_principal(mut self, pk_bytes: [u8; 32]) -> Self {
192 self.trusted_principal_pks.push(pk_bytes);
193 self
194 }
195
196 pub fn name(&self) -> &str {
197 &self.name
198 }
199
200 pub fn check_chain(&self, chain: &DyoloChain) -> Result<(), PolicyViolation> {
201 if !self.trusted_principal_pks.is_empty() {
202 let pk = chain.principal_pk.as_bytes();
203 let trusted = self.trusted_principal_pks.iter().any(|t| t == pk);
204 if !trusted {
205 return Err(PolicyViolation::PrincipalNotTrusted);
206 }
207 }
208
209 if let Some(max_depth) = self.max_chain_depth {
210 if chain.len() > max_depth as usize {
211 return Err(PolicyViolation::ChainDepthExceeded {
212 allowed: max_depth,
213 actual: chain.len(),
214 });
215 }
216 }
217
218 for cert in chain.certs() {
219 self.check_cert(cert)?;
220 }
221
222 Ok(())
223 }
224
225 pub fn check_cert(&self, cert: &DelegationCert) -> Result<(), PolicyViolation> {
226 if let Some(max_ttl) = self.max_ttl_secs {
227 let ttl = cert.expiration_unix.saturating_sub(cert.issued_at);
228 if ttl > max_ttl {
229 return Err(PolicyViolation::TtlExceedsMaximum {
230 allowed_secs: max_ttl,
231 actual_secs: ttl,
232 });
233 }
234 }
235
236 if !self.allow_sub_delegation {
237 let has_sub =
238 !cert.scope_proof.subset_intents.is_empty() || !cert.scope_proof.proofs.is_empty();
239 if has_sub && cert.max_depth > 0 {
240 return Err(PolicyViolation::SubDelegationForbidden);
241 }
242 }
243
244 #[cfg(feature = "wire")]
245 {
246 for key in &self.required_extensions {
247 if cert.extensions.get(key).is_none() {
248 return Err(PolicyViolation::RequiredExtensionAbsent { key: key.clone() });
249 }
250 }
251 }
252 #[cfg(not(feature = "wire"))]
253 {
254 if !self.required_extensions.is_empty() && cert.extensions_hash.is_none() {
255 return Err(PolicyViolation::RequiredExtensionAbsent {
256 key: "extensions missing (compile with wire feature to inspect)".into(),
257 });
258 }
259 }
260
261 Ok(())
262 }
263
264 pub fn check_intent(&self, intent: &Intent) -> Result<(), PolicyViolation> {
265 if let Some(caps) = &self.capabilities {
266 if !caps.permits(&intent.action) {
267 return Err(PolicyViolation::IntentNotAllowed {
268 intent: intent.action.clone(),
269 policy: self.name.clone(),
270 });
271 }
272 }
273 Ok(())
274 }
275}
276
277#[derive(Debug, Default)]
287pub struct PolicySet {
288 policies: Vec<DelegationPolicy>,
289}
290
291impl PolicySet {
292 pub fn new() -> Self {
293 Self::default()
294 }
295
296 #[allow(clippy::should_implement_trait)]
297 pub fn add(mut self, policy: DelegationPolicy) -> Self {
298 self.policies.push(policy);
299 self
300 }
301
302 pub fn check_chain(&self, chain: &DyoloChain) -> Result<(), A1Error> {
303 for policy in &self.policies {
304 policy
305 .check_chain(chain)
306 .map_err(|v| A1Error::PolicyViolation(v.to_string()))?;
307 }
308 Ok(())
309 }
310
311 pub fn check_intent(&self, intent: &Intent) -> Result<(), A1Error> {
312 for policy in &self.policies {
313 policy
314 .check_intent(intent)
315 .map_err(|v| A1Error::PolicyViolation(v.to_string()))?;
316 }
317 Ok(())
318 }
319
320 #[cfg(feature = "policy-yaml")]
324 #[cfg_attr(docsrs, doc(cfg(feature = "policy-yaml")))]
325 pub fn from_yaml(yaml: &str) -> Result<Self, serde_yaml::Error> {
326 #[derive(serde::Deserialize)]
327 #[serde(untagged)]
328 enum PolicyInput {
329 Single(DelegationPolicy),
330 List(Vec<DelegationPolicy>),
331 }
332
333 let input: PolicyInput = serde_yaml::from_str(yaml)?;
334 match input {
335 PolicyInput::Single(p) => Ok(Self { policies: vec![p] }),
336 PolicyInput::List(policies) => Ok(Self { policies }),
337 }
338 }
339
340 #[cfg(feature = "serde")]
342 pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
343 #[derive(serde::Deserialize)]
344 #[serde(untagged)]
345 enum PolicyInput {
346 Single(DelegationPolicy),
347 List(Vec<DelegationPolicy>),
348 }
349
350 let input: PolicyInput = serde_json::from_str(json)?;
351 match input {
352 PolicyInput::Single(p) => Ok(Self { policies: vec![p] }),
353 PolicyInput::List(policies) => Ok(Self { policies }),
354 }
355 }
356
357 #[cfg(feature = "policy-yaml")]
359 pub fn to_yaml(&self) -> Result<String, serde_yaml::Error> {
360 serde_yaml::to_string(&self.policies)
361 }
362}
363
364#[cfg(test)]
367mod tests {
368 use super::*;
369 #[allow(deprecated)]
370 use crate::{
371 cert::CertBuilder,
372 chain::DyoloChain,
373 identity::DyoloIdentity,
374 intent::{intent_hash, IntentTree},
375 };
376
377 #[allow(deprecated)]
378 fn make_chain(depth: usize, ttl: u64) -> DyoloChain {
379 let root = DyoloIdentity::generate();
380 let scope = IntentTree::build(vec![intent_hash("trade.equity", b"")])
381 .unwrap()
382 .root();
383 let now = 1_700_000_000u64;
384 let mut chain = DyoloChain::new(root.verifying_key(), scope);
385 let mut prev = root;
386 for _ in 0..depth {
387 let next = DyoloIdentity::generate();
388 let cert = CertBuilder::new(next.verifying_key(), scope, now, now + ttl).sign(&prev);
389 chain.push(cert);
390 prev = next;
391 }
392 chain
393 }
394
395 #[test]
396 fn depth_policy_enforced() {
397 let chain = make_chain(3, 3600);
398 let policy = DelegationPolicy::new("test").max_chain_depth(2);
399 assert!(policy.check_chain(&chain).is_err());
400 let policy = DelegationPolicy::new("test").max_chain_depth(5);
401 assert!(policy.check_chain(&chain).is_ok());
402 }
403
404 #[test]
405 fn ttl_policy_enforced() {
406 let chain = make_chain(1, 7200);
407 let policy = DelegationPolicy::new("test").max_ttl_secs(3600);
408 assert!(policy.check_chain(&chain).is_err());
409 }
410
411 #[test]
412 fn capability_set_prefix_matching() {
413 let caps = CapabilitySet::new().allow("trade.").allow("query");
414 assert!(caps.permits("trade.equity"));
415 assert!(caps.permits("trade.fx"));
416 assert!(caps.permits("query"));
417 assert!(!caps.permits("admin.delete"));
418 }
419
420 #[test]
421 fn wildcard_permits_all() {
422 let caps = CapabilitySet::wildcard();
423 assert!(caps.permits("anything.at.all"));
424 }
425
426 #[test]
427 fn intent_checked_against_capabilities() {
428 let policy =
429 DelegationPolicy::new("test").capabilities(CapabilitySet::new().allow("trade.equity"));
430 let allowed = Intent::new("trade.equity").unwrap();
431 let forbidden = Intent::new("admin.delete").unwrap();
432 assert!(policy.check_intent(&allowed).is_ok());
433 assert!(policy.check_intent(&forbidden).is_err());
434 }
435}