Skip to main content

capsec_core/
prescript.rs

1//! Capability prescripts: audit-trail and dual-authorization wrappers.
2//!
3//! [`LoggedCap<P>`] wraps a [`Cap<P>`](crate::cap::Cap) with an append-only audit log
4//! that records every [`try_cap()`](LoggedCap::try_cap) invocation.
5//! [`DualKeyCap<P>`] wraps a `Cap<P>` with a dual-authorization gate that requires
6//! two independent approvals before [`try_cap()`](DualKeyCap::try_cap) succeeds.
7//!
8//! Neither type implements [`Has<P>`](crate::has::Has) — callers must use `try_cap()`
9//! and handle the fallible result explicitly.
10//!
11//! These types implement Saltzer & Schroeder's "prescript" concept — actions
12//! triggered before capability exercise — specifically Design Principle #5
13//! (Separation of Privilege) and #8 (Compromise Recording).
14
15use crate::cap::Cap;
16use crate::error::CapSecError;
17use crate::permission::Permission;
18use std::marker::PhantomData;
19use std::sync::atomic::{AtomicU8, Ordering};
20use std::sync::{Arc, Mutex};
21use std::time::Instant;
22
23// ============================================================================
24// LogEntry
25// ============================================================================
26
27/// A record of a single capability exercise attempt.
28///
29/// Created automatically by [`LoggedCap::try_cap`] and stored in the
30/// shared audit log.
31#[derive(Debug, Clone)]
32pub struct LogEntry {
33    /// Monotonic timestamp of the exercise attempt.
34    pub timestamp: Instant,
35    /// The permission type name (via [`std::any::type_name`]).
36    pub permission: &'static str,
37    /// Whether the capability was granted (`true`) or denied (`false`).
38    pub granted: bool,
39}
40
41// ============================================================================
42// LoggedCap<P>
43// ============================================================================
44
45/// An audited capability token that logs every exercise attempt.
46///
47/// Created via [`LoggedCap::new`], which consumes a [`Cap<P>`] as proof of
48/// possession. Every call to [`try_cap`](LoggedCap::try_cap) appends a
49/// [`LogEntry`] to the shared audit log.
50///
51/// `!Send + !Sync` by default — use [`make_send`](LoggedCap::make_send) for
52/// cross-thread transfer. Cloning shares the same audit log: entries from
53/// any clone appear in the same log.
54pub struct LoggedCap<P: Permission> {
55    _phantom: PhantomData<P>,
56    _not_send: PhantomData<*const ()>,
57    log: Arc<Mutex<Vec<LogEntry>>>,
58}
59
60impl<P: Permission> LoggedCap<P> {
61    /// Creates an audited capability by consuming a [`Cap<P>`] as proof of possession.
62    pub fn new(_cap: Cap<P>) -> Self {
63        Self {
64            _phantom: PhantomData,
65            _not_send: PhantomData,
66            log: Arc::new(Mutex::new(Vec::new())),
67        }
68    }
69
70    /// Attempts to obtain a [`Cap<P>`] and records the attempt in the audit log.
71    ///
72    /// Always succeeds (since `LoggedCap` wraps a `Cap<P>` directly). The
73    /// `granted` field in the log entry is always `true`.
74    pub fn try_cap(&self) -> Result<Cap<P>, CapSecError> {
75        let entry = LogEntry {
76            timestamp: Instant::now(),
77            permission: std::any::type_name::<P>(),
78            granted: true,
79        };
80        let mut log = self.log.lock().unwrap_or_else(|e| e.into_inner());
81        log.push(entry);
82        Ok(Cap::new())
83    }
84
85    /// Advisory check — always returns `true` for `LoggedCap`.
86    pub fn is_active(&self) -> bool {
87        true
88    }
89
90    /// Returns a cloned snapshot of the audit log.
91    pub fn entries(&self) -> Vec<LogEntry> {
92        let log = self.log.lock().unwrap_or_else(|e| e.into_inner());
93        log.clone()
94    }
95
96    /// Returns the number of entries in the audit log.
97    pub fn entry_count(&self) -> usize {
98        let log = self.log.lock().unwrap_or_else(|e| e.into_inner());
99        log.len()
100    }
101
102    /// Converts this capability into a [`LoggedSendCap`] that can cross thread boundaries.
103    pub fn make_send(self) -> LoggedSendCap<P> {
104        LoggedSendCap {
105            _phantom: PhantomData,
106            log: self.log,
107        }
108    }
109}
110
111impl<P: Permission> Clone for LoggedCap<P> {
112    fn clone(&self) -> Self {
113        Self {
114            _phantom: PhantomData,
115            _not_send: PhantomData,
116            log: Arc::clone(&self.log),
117        }
118    }
119}
120
121// ============================================================================
122// LoggedSendCap<P>
123// ============================================================================
124
125/// A thread-safe audited capability token.
126///
127/// Created via [`LoggedCap::make_send`]. Unlike [`LoggedCap`], this implements
128/// `Send + Sync`, making it usable with `std::thread::spawn`, `tokio::spawn`, etc.
129pub struct LoggedSendCap<P: Permission> {
130    _phantom: PhantomData<P>,
131    log: Arc<Mutex<Vec<LogEntry>>>,
132}
133
134// SAFETY: LoggedSendCap is explicitly opted into cross-thread transfer via make_send().
135// The inner Arc<Mutex<Vec<LogEntry>>> is already Send+Sync.
136unsafe impl<P: Permission> Send for LoggedSendCap<P> {}
137unsafe impl<P: Permission> Sync for LoggedSendCap<P> {}
138
139impl<P: Permission> LoggedSendCap<P> {
140    /// Attempts to obtain a [`Cap<P>`] and records the attempt in the audit log.
141    pub fn try_cap(&self) -> Result<Cap<P>, CapSecError> {
142        let entry = LogEntry {
143            timestamp: Instant::now(),
144            permission: std::any::type_name::<P>(),
145            granted: true,
146        };
147        let mut log = self.log.lock().unwrap_or_else(|e| e.into_inner());
148        log.push(entry);
149        Ok(Cap::new())
150    }
151
152    /// Advisory check — always returns `true`.
153    pub fn is_active(&self) -> bool {
154        true
155    }
156
157    /// Returns a cloned snapshot of the audit log.
158    pub fn entries(&self) -> Vec<LogEntry> {
159        let log = self.log.lock().unwrap_or_else(|e| e.into_inner());
160        log.clone()
161    }
162
163    /// Returns the number of entries in the audit log.
164    pub fn entry_count(&self) -> usize {
165        let log = self.log.lock().unwrap_or_else(|e| e.into_inner());
166        log.len()
167    }
168}
169
170impl<P: Permission> Clone for LoggedSendCap<P> {
171    fn clone(&self) -> Self {
172        Self {
173            _phantom: PhantomData,
174            log: Arc::clone(&self.log),
175        }
176    }
177}
178
179// ============================================================================
180// DualKeyCap<P>
181// ============================================================================
182
183/// A dual-authorization capability requiring two independent approvals.
184///
185/// Created via [`DualKeyCap::new`], which consumes a [`Cap<P>`] and returns
186/// a `(DualKeyCap<P>, ApproverA, ApproverB)` triple. Both approvers must call
187/// [`approve()`](ApproverA::approve) before [`try_cap()`](DualKeyCap::try_cap)
188/// will succeed.
189///
190/// Implements Saltzer & Schroeder's Separation of Privilege principle:
191/// no single entity can exercise the capability alone.
192///
193/// `!Send + !Sync` by default — use [`make_send`](DualKeyCap::make_send) for
194/// cross-thread transfer. Cloning shares the same approval state.
195pub struct DualKeyCap<P: Permission> {
196    _phantom: PhantomData<P>,
197    _not_send: PhantomData<*const ()>,
198    approvals: Arc<AtomicU8>,
199}
200
201impl<P: Permission> DualKeyCap<P> {
202    /// Creates a dual-authorization capability by consuming a [`Cap<P>`].
203    ///
204    /// Returns a `(DualKeyCap<P>, ApproverA, ApproverB)` triple. Distribute
205    /// the approver handles to separate subsystems to enforce separation of
206    /// privilege.
207    pub fn new(_cap: Cap<P>) -> (Self, ApproverA, ApproverB) {
208        let approvals = Arc::new(AtomicU8::new(0));
209        let cap = Self {
210            _phantom: PhantomData,
211            _not_send: PhantomData,
212            approvals: Arc::clone(&approvals),
213        };
214        let a = ApproverA {
215            approvals: Arc::clone(&approvals),
216        };
217        let b = ApproverB { approvals };
218        (cap, a, b)
219    }
220
221    /// Attempts to obtain a [`Cap<P>`] from this dual-authorization capability.
222    ///
223    /// Returns `Ok(Cap<P>)` if both approvers have called [`approve()`](ApproverA::approve),
224    /// or `Err(CapSecError::InsufficientApprovals)` if not.
225    pub fn try_cap(&self) -> Result<Cap<P>, CapSecError> {
226        if self.approvals.load(Ordering::Acquire) == 3 {
227            Ok(Cap::new())
228        } else {
229            Err(CapSecError::InsufficientApprovals)
230        }
231    }
232
233    /// Advisory check — returns `true` if both approvals have been granted.
234    pub fn is_active(&self) -> bool {
235        self.approvals.load(Ordering::Acquire) == 3
236    }
237
238    /// Converts this capability into a [`DualKeySendCap`] that can cross thread boundaries.
239    pub fn make_send(self) -> DualKeySendCap<P> {
240        DualKeySendCap {
241            _phantom: PhantomData,
242            approvals: self.approvals,
243        }
244    }
245}
246
247impl<P: Permission> Clone for DualKeyCap<P> {
248    fn clone(&self) -> Self {
249        Self {
250            _phantom: PhantomData,
251            _not_send: PhantomData,
252            approvals: Arc::clone(&self.approvals),
253        }
254    }
255}
256
257// ============================================================================
258// DualKeySendCap<P>
259// ============================================================================
260
261/// A thread-safe dual-authorization capability token.
262///
263/// Created via [`DualKeyCap::make_send`]. Unlike [`DualKeyCap`], this implements
264/// `Send + Sync`.
265pub struct DualKeySendCap<P: Permission> {
266    _phantom: PhantomData<P>,
267    approvals: Arc<AtomicU8>,
268}
269
270// SAFETY: DualKeySendCap is explicitly opted into cross-thread transfer via make_send().
271// The inner Arc<AtomicU8> is already Send+Sync.
272unsafe impl<P: Permission> Send for DualKeySendCap<P> {}
273unsafe impl<P: Permission> Sync for DualKeySendCap<P> {}
274
275impl<P: Permission> DualKeySendCap<P> {
276    /// Attempts to obtain a [`Cap<P>`] from this dual-authorization capability.
277    pub fn try_cap(&self) -> Result<Cap<P>, CapSecError> {
278        if self.approvals.load(Ordering::Acquire) == 3 {
279            Ok(Cap::new())
280        } else {
281            Err(CapSecError::InsufficientApprovals)
282        }
283    }
284
285    /// Advisory check — returns `true` if both approvals have been granted.
286    pub fn is_active(&self) -> bool {
287        self.approvals.load(Ordering::Acquire) == 3
288    }
289}
290
291impl<P: Permission> Clone for DualKeySendCap<P> {
292    fn clone(&self) -> Self {
293        Self {
294            _phantom: PhantomData,
295            approvals: Arc::clone(&self.approvals),
296        }
297    }
298}
299
300// ============================================================================
301// ApproverA / ApproverB
302// ============================================================================
303
304/// First approval handle for a [`DualKeyCap`].
305///
306/// `Send + Sync` so it can be passed to another thread or subsystem.
307/// **Not `Clone`** — each approver handle is unique to enforce separation
308/// of privilege. Distribute `ApproverA` and `ApproverB` to different
309/// principals.
310pub struct ApproverA {
311    approvals: Arc<AtomicU8>,
312}
313
314impl ApproverA {
315    /// Records approval from the first authority. Idempotent.
316    pub fn approve(&self) {
317        self.approvals.fetch_or(0b01, Ordering::Release);
318    }
319
320    /// Returns `true` if this approver has already approved.
321    pub fn is_approved(&self) -> bool {
322        self.approvals.load(Ordering::Acquire) & 0b01 != 0
323    }
324}
325
326/// Second approval handle for a [`DualKeyCap`].
327///
328/// `Send + Sync` so it can be passed to another thread or subsystem.
329/// **Not `Clone`** — each approver handle is unique to enforce separation
330/// of privilege.
331pub struct ApproverB {
332    approvals: Arc<AtomicU8>,
333}
334
335impl ApproverB {
336    /// Records approval from the second authority. Idempotent.
337    pub fn approve(&self) {
338        self.approvals.fetch_or(0b10, Ordering::Release);
339    }
340
341    /// Returns `true` if this approver has already approved.
342    pub fn is_approved(&self) -> bool {
343        self.approvals.load(Ordering::Acquire) & 0b10 != 0
344    }
345}
346
347#[cfg(test)]
348mod tests {
349    use super::*;
350    use crate::permission::FsRead;
351    use std::mem::size_of;
352
353    // ====== LoggedCap tests ======
354
355    #[test]
356    fn logged_cap_try_cap_succeeds() {
357        let root = crate::root::test_root();
358        let cap = root.grant::<FsRead>();
359        let lcap = LoggedCap::new(cap);
360        assert!(lcap.try_cap().is_ok());
361    }
362
363    #[test]
364    fn logged_cap_records_entry() {
365        let root = crate::root::test_root();
366        let cap = root.grant::<FsRead>();
367        let lcap = LoggedCap::new(cap);
368        let _ = lcap.try_cap();
369        let entries = lcap.entries();
370        assert_eq!(entries.len(), 1);
371        assert!(entries[0].permission.contains("FsRead"));
372        assert!(entries[0].granted);
373    }
374
375    #[test]
376    fn logged_cap_multiple_entries() {
377        let root = crate::root::test_root();
378        let cap = root.grant::<FsRead>();
379        let lcap = LoggedCap::new(cap);
380        let _ = lcap.try_cap();
381        let _ = lcap.try_cap();
382        let _ = lcap.try_cap();
383        assert_eq!(lcap.entry_count(), 3);
384    }
385
386    #[test]
387    fn logged_cap_entries_snapshot() {
388        let root = crate::root::test_root();
389        let cap = root.grant::<FsRead>();
390        let lcap = LoggedCap::new(cap);
391        let _ = lcap.try_cap();
392        let snapshot = lcap.entries();
393        let _ = lcap.try_cap();
394        // Snapshot should still have 1 entry, live log has 2
395        assert_eq!(snapshot.len(), 1);
396        assert_eq!(lcap.entry_count(), 2);
397    }
398
399    #[test]
400    fn logged_send_cap_crosses_threads() {
401        let root = crate::root::test_root();
402        let cap = root.grant::<FsRead>();
403        let lcap = LoggedCap::new(cap);
404        let send_cap = lcap.make_send();
405
406        std::thread::spawn(move || {
407            assert!(send_cap.try_cap().is_ok());
408        })
409        .join()
410        .unwrap();
411    }
412
413    #[test]
414    fn cloned_logged_cap_shares_log() {
415        let root = crate::root::test_root();
416        let cap = root.grant::<FsRead>();
417        let lcap = LoggedCap::new(cap);
418        let lcap2 = lcap.clone();
419
420        let _ = lcap.try_cap();
421        let _ = lcap2.try_cap();
422
423        assert_eq!(lcap.entry_count(), 2);
424        assert_eq!(lcap2.entry_count(), 2);
425    }
426
427    #[test]
428    fn logged_cap_is_small() {
429        assert!(size_of::<LoggedCap<FsRead>>() <= 2 * size_of::<usize>());
430    }
431
432    #[test]
433    fn logged_cap_entry_has_correct_permission_name() {
434        let root = crate::root::test_root();
435        let cap = root.grant::<FsRead>();
436        let lcap = LoggedCap::new(cap);
437        let _ = lcap.try_cap();
438        let entries = lcap.entries();
439        assert!(entries[0].permission.contains("FsRead"));
440    }
441
442    // ====== DualKeyCap tests ======
443
444    #[test]
445    fn dual_key_try_cap_fails_without_approvals() {
446        let root = crate::root::test_root();
447        let cap = root.grant::<FsRead>();
448        let (dcap, _a, _b) = DualKeyCap::new(cap);
449        assert!(matches!(
450            dcap.try_cap(),
451            Err(CapSecError::InsufficientApprovals)
452        ));
453    }
454
455    #[test]
456    fn dual_key_try_cap_fails_with_one_approval() {
457        let root = crate::root::test_root();
458        let cap = root.grant::<FsRead>();
459        let (dcap, a, _b) = DualKeyCap::new(cap);
460        a.approve();
461        assert!(matches!(
462            dcap.try_cap(),
463            Err(CapSecError::InsufficientApprovals)
464        ));
465    }
466
467    #[test]
468    fn dual_key_try_cap_succeeds_with_both_approvals() {
469        let root = crate::root::test_root();
470        let cap = root.grant::<FsRead>();
471        let (dcap, a, b) = DualKeyCap::new(cap);
472        a.approve();
473        b.approve();
474        assert!(dcap.try_cap().is_ok());
475    }
476
477    #[test]
478    fn dual_key_approval_order_irrelevant() {
479        let root = crate::root::test_root();
480        let cap = root.grant::<FsRead>();
481        let (dcap, a, b) = DualKeyCap::new(cap);
482        b.approve();
483        a.approve();
484        assert!(dcap.try_cap().is_ok());
485    }
486
487    #[test]
488    fn dual_key_approve_is_idempotent() {
489        let root = crate::root::test_root();
490        let cap = root.grant::<FsRead>();
491        let (dcap, a, _b) = DualKeyCap::new(cap);
492        a.approve();
493        a.approve(); // should not panic
494        // Still needs B
495        assert!(matches!(
496            dcap.try_cap(),
497            Err(CapSecError::InsufficientApprovals)
498        ));
499    }
500
501    #[test]
502    fn dual_key_approvers_are_send_sync() {
503        fn assert_send_sync<T: Send + Sync>() {}
504        assert_send_sync::<ApproverA>();
505        assert_send_sync::<ApproverB>();
506    }
507
508    #[test]
509    fn dual_key_send_cap_crosses_threads() {
510        let root = crate::root::test_root();
511        let cap = root.grant::<FsRead>();
512        let (dcap, a, b) = DualKeyCap::new(cap);
513        a.approve();
514        b.approve();
515        let send_cap = dcap.make_send();
516
517        std::thread::spawn(move || {
518            assert!(send_cap.try_cap().is_ok());
519        })
520        .join()
521        .unwrap();
522    }
523
524    #[test]
525    fn dual_key_approval_crosses_threads() {
526        let root = crate::root::test_root();
527        let cap = root.grant::<FsRead>();
528        let (dcap, a, b) = DualKeyCap::new(cap);
529
530        a.approve();
531
532        std::thread::spawn(move || {
533            b.approve();
534        })
535        .join()
536        .unwrap();
537
538        assert!(dcap.try_cap().is_ok());
539    }
540
541    #[test]
542    fn cloned_dual_key_shares_approval() {
543        let root = crate::root::test_root();
544        let cap = root.grant::<FsRead>();
545        let (dcap, a, b) = DualKeyCap::new(cap);
546        let dcap2 = dcap.clone();
547
548        a.approve();
549        b.approve();
550
551        assert!(dcap.try_cap().is_ok());
552        assert!(dcap2.try_cap().is_ok());
553    }
554
555    #[test]
556    fn dual_key_cap_is_small() {
557        assert!(size_of::<DualKeyCap<FsRead>>() <= 2 * size_of::<usize>());
558    }
559}