Skip to main content

active_rpc/
cap.rs

1//! Capabilities: unforgeable, attenuable references.
2//!
3//! A capability is an opaque 128-bit id minted by a kernel from a per-kernel
4//! secret, a monotonic serial and the parent id. Holding the id is the *only*
5//! authority needed to call the object it names, and the kernel is the only
6//! component that can mint one: an id that is not in the registry is refused
7//! with `rpc/unknown-capability`, so a peer cannot forge, guess or transplant a
8//! capability from another connection.
9//!
10//! Attenuation is monotone by construction: [`CapAttrs::attenuate`] refuses any
11//! attempt to widen methods, budget, path scope or tool policy.
12
13use std::collections::BTreeSet;
14use std::fmt;
15use std::rc::Rc;
16
17use crate::budget::Budget;
18use crate::error::{Error, Result};
19
20/// An unforgeable capability identifier.
21#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
22pub struct CapId(pub [u8; 16]);
23
24impl CapId {
25    /// Derive an id from the kernel secret, serial and parent.
26    ///
27    /// Deterministic for a given kernel (which makes tests and replays stable),
28    /// unpredictable to a peer without the secret.
29    pub fn derive(secret: &[u8; 32], serial: u64, parent: Option<&CapId>) -> CapId {
30        let mut hasher = blake3::Hasher::new();
31        hasher.update(b"active-rpc/cap");
32        hasher.update(secret);
33        hasher.update(&serial.to_le_bytes());
34        hasher.update(&parent.map(|id| id.0).unwrap_or([0u8; 16]));
35        let digest = hasher.finalize();
36        let mut id = [0u8; 16];
37        id.copy_from_slice(&digest.as_bytes()[..16]);
38        CapId(id)
39    }
40
41    /// Hex form for logs and errors.
42    pub fn hex(self) -> String {
43        let mut out = String::with_capacity(32);
44        for byte in self.0 {
45            out.push_str(&format!("{byte:02x}"));
46        }
47        out
48    }
49}
50
51impl fmt::Debug for CapId {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        write!(f, "CapId({})", self.hex())
54    }
55}
56
57impl fmt::Display for CapId {
58    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59        f.write_str(&self.hex())
60    }
61}
62
63/// Identity of a provider object: which adapter, which object within it.
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
65pub struct ProviderId(pub u64);
66
67/// Tool-level policy attached to a capability.
68#[derive(Debug, Clone, PartialEq, Eq, Default)]
69pub struct ToolPolicy {
70    /// Tool names this capability may invoke. Empty means "none".
71    pub allowed: BTreeSet<String>,
72    /// Maximum bytes one tool call may pass.
73    pub max_arg_bytes: u64,
74    /// Whether the tool may run a local process.
75    pub allow_process: bool,
76}
77
78impl ToolPolicy {
79    /// Build a policy from tool names.
80    pub fn allow(names: &[&str]) -> Self {
81        Self {
82            allowed: names.iter().map(|name| name.to_string()).collect(),
83            max_arg_bytes: 1 << 20,
84            allow_process: false,
85        }
86    }
87
88    /// True when `self` grants nothing beyond `parent`.
89    pub fn is_attenuation_of(&self, parent: &ToolPolicy) -> bool {
90        self.allowed.is_subset(&parent.allowed)
91            && self.max_arg_bytes <= parent.max_arg_bytes
92            && (!self.allow_process || parent.allow_process)
93    }
94}
95
96/// The authority a capability carries.
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct CapAttrs {
99    /// Granted method names. Must be non-empty after attenuation.
100    pub methods: BTreeSet<String>,
101    /// Resource budget.
102    pub budget: Budget,
103    /// May not mutate module state.
104    pub read_only: bool,
105    /// Redacts trace output.
106    pub redact: bool,
107    /// Dotted path prefix the capability is confined to (inclusive).
108    pub path_scope: Option<String>,
109    /// Tool policy, when the capability targets a tool.
110    pub tool_policy: Option<ToolPolicy>,
111}
112
113impl CapAttrs {
114    /// Full authority over `methods` with the default budget.
115    pub fn new(methods: &[&str]) -> Self {
116        Self {
117            methods: methods.iter().map(|method| method.to_string()).collect(),
118            budget: Budget::default(),
119            read_only: false,
120            redact: false,
121            path_scope: None,
122            tool_policy: None,
123        }
124    }
125
126    /// Restrict methods.
127    pub fn with_methods(mut self, methods: &[&str]) -> Self {
128        self.methods = methods.iter().map(|method| method.to_string()).collect();
129        self
130    }
131
132    /// Restrict the budget.
133    pub fn with_budget(mut self, budget: Budget) -> Self {
134        self.budget = budget;
135        self
136    }
137
138    /// Confine the capability to a path prefix.
139    pub fn with_path(mut self, path: impl Into<String>) -> Self {
140        self.path_scope = Some(path.into());
141        self
142    }
143
144    /// Mark the capability read-only.
145    pub fn read_only(mut self) -> Self {
146        self.read_only = true;
147        self
148    }
149
150    /// Mark the capability redacting.
151    pub fn redacted(mut self) -> Self {
152        self.redact = true;
153        self
154    }
155
156    /// Attach a tool policy.
157    pub fn with_tool_policy(mut self, policy: ToolPolicy) -> Self {
158        self.tool_policy = Some(policy);
159        self
160    }
161
162    /// True when `self` is a valid attenuation of `parent`.
163    pub fn is_attenuation_of(&self, parent: &CapAttrs) -> bool {
164        let methods_ok = self.methods.is_subset(&parent.methods) && !self.methods.is_empty();
165        let budget_ok = self.budget.is_attenuation_of(&parent.budget);
166        let read_only_ok = !parent.read_only || self.read_only;
167        let redact_ok = !parent.redact || self.redact;
168        let path_ok = match (&self.path_scope, &parent.path_scope) {
169            (None, None) => true,
170            (Some(_), None) => true,
171            (Some(child), Some(parent)) => is_path_prefix(parent, child),
172            (None, Some(_)) => false,
173        };
174        let tool_ok = match (&self.tool_policy, &parent.tool_policy) {
175            // A parent without a policy leaves the tool surface open; adding one
176            // on the child narrows it, which `attenuate` also accepts.
177            (_, None) => true,
178            // Dropping the parent's policy widens authority.
179            (None, Some(_)) => false,
180            (Some(policy), Some(parent)) => policy.is_attenuation_of(parent),
181        };
182        methods_ok && budget_ok && read_only_ok && redact_ok && path_ok && tool_ok
183    }
184
185    /// Attenuate, refusing anything that would widen authority.
186    pub fn attenuate(&self, child: CapAttrs) -> Result<CapAttrs> {
187        if child.methods.is_empty() {
188            return Err(Error::InvalidArgument("attenuation grants no methods"));
189        }
190        if !child.methods.is_subset(&self.methods) {
191            return Err(Error::Escalation("method set grows"));
192        }
193        if !child.budget.is_attenuation_of(&self.budget) {
194            return Err(Error::Escalation("budget grows"));
195        }
196        if self.read_only && !child.read_only {
197            return Err(Error::Escalation("read-only cannot be lifted"));
198        }
199        if self.redact && !child.redact {
200            return Err(Error::Escalation("redaction cannot be lifted"));
201        }
202        match (&child.path_scope, &self.path_scope) {
203            (_, None) => {}
204            (Some(child_path), Some(parent)) if is_path_prefix(parent, child_path) => {}
205            (Some(_), Some(_)) => return Err(Error::Escalation("path scope widens")),
206            (None, Some(_)) => return Err(Error::Escalation("path scope removed")),
207        }
208        match (&child.tool_policy, &self.tool_policy) {
209            (_, None) => {}
210            (Some(policy), Some(parent)) if policy.is_attenuation_of(parent) => {}
211            _ => return Err(Error::Escalation("tool policy widens")),
212        }
213        let mut attrs = child;
214        attrs.budget = attrs.budget.intersect(&self.budget);
215        Ok(attrs)
216    }
217
218    /// Whether `method` is granted.
219    pub fn grants(&self, method: &str) -> bool {
220        self.methods.contains(method)
221    }
222
223    /// Whether `path` is inside the capability's scope.
224    pub fn covers_path(&self, path: &str) -> bool {
225        match &self.path_scope {
226            None => true,
227            Some(scope) => is_path_prefix(scope, path),
228        }
229    }
230}
231
232/// True when `parent` is a dotted-path prefix of `child` (or equal).
233pub fn is_path_prefix(parent: &str, child: &str) -> bool {
234    if parent == child {
235        return true;
236    }
237    child
238        .strip_prefix(parent)
239        .map(|rest| rest.starts_with('.'))
240        .unwrap_or(false)
241}
242
243/// Provider-side record of a live capability.
244#[derive(Debug, Clone, PartialEq, Eq)]
245pub struct CapRecord {
246    /// Capability id.
247    pub id: CapId,
248    /// Provider that owns the object.
249    pub provider: ProviderId,
250    /// Object within the provider (module id, tool id, lane id).
251    pub object: u64,
252    /// Authority.
253    pub attrs: CapAttrs,
254    /// Parent capability, when this one was derived by attenuation.
255    pub parent: Option<CapId>,
256    /// Outstanding references (client handles plus transfers).
257    pub refs: u32,
258    /// Revoked capabilities are refused even if the id is known.
259    pub revoked: bool,
260}
261
262impl CapRecord {
263    /// A first-generation capability.
264    pub fn new(id: CapId, provider: ProviderId, object: u64, attrs: CapAttrs) -> Self {
265        Self {
266            id,
267            provider,
268            object,
269            attrs,
270            parent: None,
271            refs: 1,
272            revoked: false,
273        }
274    }
275}
276
277/// Control surface a capability handle uses to release its reference.
278///
279/// The kernel implements this; `cap.rs` stays independent of the kernel so the
280/// two can be reasoned about separately.
281pub trait CapControl {
282    /// Add one reference to a capability.
283    fn retain(&self, id: CapId);
284    /// Drop one reference; the provider is notified when the last one goes.
285    fn release(&self, id: CapId);
286}
287
288/// A client-side capability handle.
289///
290/// Cloning a handle adds a reference; dropping the last handle notifies the
291/// provider (automatic lifetime, `RPROPOSAL` §3).
292pub struct CapHandle {
293    control: Rc<dyn CapControl>,
294    id: CapId,
295    /// False only for handles that do not own a reference (imported views).
296    owning: bool,
297}
298
299impl CapHandle {
300    /// Wrap a minted capability.
301    pub fn new(control: Rc<dyn CapControl>, id: CapId) -> Self {
302        Self {
303            control,
304            id,
305            owning: true,
306        }
307    }
308
309    /// A non-owning handle (used when reconstructing a handle from a transfer).
310    pub fn borrowed(control: Rc<dyn CapControl>, id: CapId) -> Self {
311        Self {
312            control,
313            id,
314            owning: false,
315        }
316    }
317
318    /// The capability id this handle refers to.
319    pub fn id(&self) -> CapId {
320        self.id
321    }
322
323    /// Surrender this handle without releasing its reference.
324    ///
325    /// Used when a reference moves (channel transfer): the receiver becomes
326    /// responsible for the reference the sender held.
327    pub fn into_id(mut self) -> CapId {
328        self.owning = false;
329        self.id
330    }
331}
332
333impl Clone for CapHandle {
334    /// Cloning an *owning* handle retains the capability: the kernel increments
335    /// the reference count and both handles must drop before the provider is
336    /// notified. Cloning a borrowed handle stays borrowed — it never retained a
337    /// reference, so it must not create one.
338    fn clone(&self) -> Self {
339        if self.owning {
340            self.control.retain(self.id);
341        }
342        Self {
343            control: Rc::clone(&self.control),
344            id: self.id,
345            owning: self.owning,
346        }
347    }
348}
349
350impl Drop for CapHandle {
351    fn drop(&mut self) {
352        if self.owning {
353            self.control.release(self.id);
354        }
355    }
356}
357
358impl fmt::Debug for CapHandle {
359    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
360        f.debug_struct("CapHandle")
361            .field("id", &self.id.hex())
362            .finish()
363    }
364}