ndn_cert/
policy.rs

1//! Namespace policies — define what certificate names a CA may issue.
2//!
3//! A CA should only issue certificates for names within its authorized namespace.
4//! Policies are checked before a challenge is even started, preventing
5//! unauthorized cross-namespace issuance.
6
7use ndn_packet::Name;
8use ndn_security::Certificate;
9
10/// Decision returned by a namespace policy evaluation.
11#[derive(Debug, Clone)]
12pub enum PolicyDecision {
13    /// Allow the request.
14    Allow,
15    /// Deny the request with a reason.
16    Deny(String),
17}
18
19/// A policy that decides whether a CA may issue a certificate for a given name.
20pub trait NamespacePolicy: Send + Sync {
21    /// Evaluate whether `requested_name` may be issued to a requester holding
22    /// `requester_cert` (may be `None` for the first enrollment).
23    fn evaluate(
24        &self,
25        requested_name: &Name,
26        requester_cert: Option<&Certificate>,
27        ca_prefix: &Name,
28    ) -> PolicyDecision;
29}
30
31/// The hierarchical policy: a requester may only obtain certificates for names
32/// that are strictly under their own current certificate's identity prefix.
33///
34/// A CA under `/com/acme/fleet/CA` may also issue to any name under
35/// `/com/acme/fleet/`.
36///
37/// # Examples
38///
39/// - `/com/acme/fleet/VIN-123` can request `/com/acme/fleet/VIN-123/ecu/brake` ✓
40/// - `/com/acme/fleet/VIN-123` cannot request `/com/acme/fleet/VIN-456/...` ✗
41/// - A new device (no cert yet) may request any name under the CA's prefix ✓
42pub struct HierarchicalPolicy;
43
44impl NamespacePolicy for HierarchicalPolicy {
45    fn evaluate(
46        &self,
47        requested_name: &Name,
48        requester_cert: Option<&Certificate>,
49        ca_prefix: &Name,
50    ) -> PolicyDecision {
51        // Extract CA identity prefix (strip /CA suffix if present)
52        let ca_identity = strip_ca_suffix(ca_prefix);
53
54        // New device (no existing cert): may request any name under the CA's namespace
55        let requester_prefix = match requester_cert {
56            None => ca_identity.clone(),
57            Some(cert) => {
58                // Strip /KEY/... suffix to get the identity name
59                strip_key_suffix(cert.name.as_ref())
60            }
61        };
62
63        if requested_name.has_prefix(&requester_prefix) {
64            PolicyDecision::Allow
65        } else {
66            PolicyDecision::Deny(format!(
67                "{} is not under requester prefix {}",
68                requested_name, requester_prefix
69            ))
70        }
71    }
72}
73
74/// A policy based on explicit delegation rules.
75///
76/// Each rule maps a requester name pattern to a set of allowed sub-prefixes.
77pub struct DelegationPolicy {
78    /// List of (requester_prefix, allowed_name_prefix) pairs.
79    pub rules: Vec<(Name, Name)>,
80    /// Whether to allow new devices (no cert) to request under any rule's allowed prefix.
81    pub allow_new_devices: bool,
82}
83
84impl DelegationPolicy {
85    pub fn new() -> Self {
86        Self {
87            rules: Vec::new(),
88            allow_new_devices: true,
89        }
90    }
91
92    /// Allow requester under `requester_prefix` to get certs under `allowed_prefix`.
93    pub fn allow(mut self, requester_prefix: Name, allowed_prefix: Name) -> Self {
94        self.rules.push((requester_prefix, allowed_prefix));
95        self
96    }
97}
98
99impl Default for DelegationPolicy {
100    fn default() -> Self {
101        Self::new()
102    }
103}
104
105impl NamespacePolicy for DelegationPolicy {
106    fn evaluate(
107        &self,
108        requested_name: &Name,
109        requester_cert: Option<&Certificate>,
110        _ca_prefix: &Name,
111    ) -> PolicyDecision {
112        match requester_cert {
113            None => {
114                if self.allow_new_devices {
115                    // Allow new devices to request under any allowed prefix
116                    for (_, allowed) in &self.rules {
117                        if requested_name.has_prefix(allowed) {
118                            return PolicyDecision::Allow;
119                        }
120                    }
121                    PolicyDecision::Deny("no matching rule for new device".to_string())
122                } else {
123                    PolicyDecision::Deny("new devices not allowed".to_string())
124                }
125            }
126            Some(cert) => {
127                let requester_identity = strip_key_suffix(cert.name.as_ref());
128                for (req_prefix, allowed_prefix) in &self.rules {
129                    if requester_identity.has_prefix(req_prefix)
130                        && requested_name.has_prefix(allowed_prefix)
131                    {
132                        return PolicyDecision::Allow;
133                    }
134                }
135                PolicyDecision::Deny("no matching delegation rule".to_string())
136            }
137        }
138    }
139}
140
141fn strip_key_suffix(name: &Name) -> Name {
142    let comps = name.components();
143    // NAME_COMPONENT type is 0x08; "KEY" component
144    let key_pos = comps
145        .iter()
146        .rposition(|c| c.typ == 0x08 && c.value.as_ref() == b"KEY");
147    match key_pos {
148        Some(pos) if pos > 0 => Name::from_components(comps[..pos].iter().cloned()),
149        _ => name.clone(),
150    }
151}
152
153fn strip_ca_suffix(name: &Name) -> Name {
154    let comps = name.components();
155    // Strip trailing /CA component if present
156    if let Some(last) = comps.last()
157        && last.typ == 0x08
158        && last.value.as_ref() == b"CA"
159    {
160        return Name::from_components(comps[..comps.len() - 1].iter().cloned());
161    }
162    name.clone()
163}