Skip to content

Commit f1ab737

Browse files
committed
feat: migrate public API from Has<P> to CapProvider<P> for transparent scope enforcement
1 parent 70a5655 commit f1ab737

37 files changed

Lines changed: 543 additions & 234 deletions

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ capsec provides four macros that work together:
164164

165165
| Macro | Purpose |
166166
|-------|---------|
167-
| `#[capsec::context]` | Generates `Has<P>` impls on a struct, turning it into a capability context |
167+
| `#[capsec::context]` | Generates `Has<P>` and `CapProvider<P>` impls on a struct, turning it into a capability context |
168168
| `#[capsec::main]` | Injects `CapRoot` creation into a function entry point |
169169
| `#[capsec::requires]` | Validates that a function's parameters satisfy declared permissions |
170170
| `#[capsec::deny]` | Marks a function as capability-free; violations are promoted to critical by the audit tool |

Cargo.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ trybuild = "1"
5151
insta = { version = "1", features = ["yaml"] }
5252

5353
# Internal crates
54-
capsec-core = { path = "crates/capsec-core", version = "0.1.0" }
55-
capsec-macro = { path = "crates/capsec-macro", version = "0.1.0" }
56-
capsec-std = { path = "crates/capsec-std", version = "0.1.0" }
57-
capsec-tokio = { path = "crates/capsec-tokio", version = "0.1.0" }
54+
capsec-core = { path = "crates/capsec-core", version = "0.1.9" }
55+
capsec-macro = { path = "crates/capsec-macro", version = "0.1.9" }
56+
capsec-std = { path = "crates/capsec-std", version = "0.1.9" }
57+
capsec-tokio = { path = "crates/capsec-tokio", version = "0.1.9" }

README.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ cargo capsec audit
2222
capsec fills that gap with three layers:
2323

2424
1. **`cargo capsec audit`** — a static audit tool that scans your code and reports every I/O call. Drop it into CI and know exactly what your dependencies do.
25-
2. **Compile-time type system** — functions declare their I/O permissions via `Has<P>` trait bounds, and the compiler rejects anything that exceeds them. Zero runtime cost.
25+
2. **Compile-time type system** — functions declare their I/O permissions via `CapProvider<P>` trait bounds, and the compiler rejects anything that exceeds them. Zero runtime cost. Scoped capabilities (`Attenuated<P, S>`) enforce *where* a capability can act.
2626
3. **Runtime capability control**`RuntimeCap` (revocable) and `TimedCap` (expiring) wrap static capabilities with runtime validity checks for dynamic scenarios like server init or migration windows.
2727

2828
The audit tool finds the problems. The type system prevents them at compile time. Runtime caps handle the cases where permissions need to change dynamically.
@@ -120,15 +120,15 @@ Functions declare their I/O requirements in the type signature. The compiler enf
120120
use capsec::prelude::*;
121121
122122
// Define a context with exactly the permissions your app needs.
123-
// The macro generates Cap fields, constructor, and Has<P> impls.
123+
// The macro generates Cap fields, constructor, Has<P> impls, and CapProvider<P> impls.
124124
#[capsec::context]
125125
struct AppCtx {
126126
fs: FsRead,
127127
net: NetConnect,
128128
}
129129
130-
// Leaf functions take &impl Has<P> — works with raw caps AND context structs.
131-
pub fn load_config(path: &str, cap: &impl Has<FsRead>) -> Result<String, CapSecError> {
130+
// Leaf functions take &impl CapProvider<P> — works with raw caps, context structs, AND scoped caps.
131+
pub fn load_config(path: &str, cap: &impl CapProvider<FsRead>) -> Result<String, CapSecError> {
132132
capsec::fs::read_to_string(path, cap)
133133
}
134134
@@ -157,11 +157,11 @@ let _ = capsec::fs::read_to_string("/etc/passwd", &net_cap);
157157
```
158158

159159
```
160-
error[E0277]: the trait bound `Cap<NetConnect>: Has<FsRead>` is not satisfied
160+
error[E0277]: the trait bound `Cap<NetConnect>: CapProvider<FsRead>` is not satisfied
161161
--> src/main.rs:4:55
162162
|
163163
4 | let _ = capsec::fs::read_to_string("/etc/passwd", &net_cap);
164-
| -------------------------- ^^^^^^^^ the trait `Has<FsRead>` is not implemented for `Cap<NetConnect>`
164+
| -------------------------- ^^^^^^^^ the trait `CapProvider<FsRead>` is not implemented for `Cap<NetConnect>`
165165
| |
166166
| required by a bound introduced by this call
167167
```
@@ -189,15 +189,15 @@ help: provide the argument
189189

190190
```rust
191191
let fs_all = root.grant::<FsAll>();
192-
needs_net(&fs_all); // fn needs_net(_: &impl Has<NetConnect>) {}
192+
needs_net(&fs_all); // fn needs_net(_: &impl CapProvider<NetConnect>) {}
193193
```
194194

195195
```
196-
error[E0277]: the trait bound `Cap<FsAll>: Has<NetConnect>` is not satisfied
196+
error[E0277]: the trait bound `Cap<FsAll>: CapProvider<NetConnect>` is not satisfied
197197
--> src/main.rs:3:15
198198
|
199199
3 | needs_net(&fs_all);
200-
| --------- ^^^^^^^ the trait `Has<NetConnect>` is not implemented for `Cap<FsAll>`
200+
| --------- ^^^^^^^ the trait `CapProvider<NetConnect>` is not implemented for `Cap<FsAll>`
201201
| |
202202
| required by a bound introduced by this call
203203
```
@@ -210,7 +210,7 @@ These are real `rustc` errors — no custom error framework, no runtime panics.
210210
|--|--------|-------|
211211
| Can any function read files? | Yes | Only if it has `Cap<FsRead>` |
212212
| Can any function open sockets? | Yes | Only if it has `Cap<NetConnect>` |
213-
| Can you audit who has what access? | Grep and pray | Grep for `Has<FsRead>` |
213+
| Can you audit who has what access? | Grep and pray | Grep for `CapProvider<FsRead>` |
214214
| Runtime cost? | N/A | Zero — all types are erased at compile time |
215215

216216
### Security model
@@ -347,7 +347,7 @@ fn main(root: CapRoot) -> Result<(), Box<dyn std::error::Error>> {
347347

348348
### Key properties
349349

350-
- `RuntimeCap`, `TimedCap`, `LoggedCap`, and `DualKeyCap` do **not** implement `Has<P>` — fallibility is explicit via `try_cap()` at every call site
350+
- `RuntimeCap`, `TimedCap`, `LoggedCap`, and `DualKeyCap` do **not** implement `Has<P>` but they do implement `CapProvider<P>` — so they can be passed directly to capsec-std/tokio functions. Fallibility is handled transparently by `provide_cap()`
351351
- All are `!Send` by default — use `make_send()` to opt into cross-thread transfer
352352
- Cloning a `RuntimeCap` shares the revocation flag — revoking one revokes all clones
353353
- Cloning a `LoggedCap` shares the audit log — entries from any clone appear in the same log
@@ -379,7 +379,7 @@ capsec's design draws from three foundational papers in capability-based securit
379379

380380
- **Saltzer & Schroeder (1975)**[The Protection of Information in Computer Systems](https://www.cs.virginia.edu/~evans/cs551/saltzer/). Defined the eight design principles for protection mechanisms. capsec implements six: economy of mechanism (zero-sized types), fail-safe defaults (no cap = no access), least privilege (the core mission), open design (open source + adversarial test suite), separation of privilege (`DualKeyCap`), and compromise recording (`LoggedCap`). The two partially met — complete mediation and least common mechanism — are inherent limitations of a library-level approach.
381381

382-
- **Melicher et al. (2017)**[A Capability-Based Module System for Authority Control](https://www.cs.cmu.edu/~aldrich/papers/ecoop17modules.pdf) (ECOOP 2017). Formalized non-transitive authority in the Wyvern language, proving that a module's authority can be determined by inspecting only its interface. capsec achieves the same property: `Has<P>` bounds make a function's authority visible in its signature, and `Attenuated<P, S>` / runtime cap types that don't implement `Has<P>` enforce non-transitivity.
382+
- **Melicher et al. (2017)**[A Capability-Based Module System for Authority Control](https://www.cs.cmu.edu/~aldrich/papers/ecoop17modules.pdf) (ECOOP 2017). Formalized non-transitive authority in the Wyvern language, proving that a module's authority can be determined by inspecting only its interface. capsec achieves the same property: `CapProvider<P>` bounds make a function's authority visible in its signature, and `Attenuated<P, S>` enforces non-transitivity through scope checks embedded in `provide_cap()`.
383383

384384
---
385385

crates/capsec-core/src/attenuate.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ impl<P: Permission> Cap<P> {
5858

5959
impl<P: Permission, S: Scope> Attenuated<P, S> {
6060
/// Checks whether `target` is within this capability's scope.
61+
#[must_use = "ignoring a scope check silently discards scope violations"]
6162
pub fn check(&self, target: &str) -> Result<(), CapSecError> {
6263
self.scope.check(target)
6364
}

crates/capsec-core/src/cap.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ use std::marker::PhantomData;
3030
/// // cap is a proof token — zero bytes at runtime
3131
/// assert_eq!(std::mem::size_of_val(&cap), 0);
3232
/// ```
33+
#[must_use = "capability tokens are proof of permission — discarding one wastes a grant"]
3334
pub struct Cap<P: Permission> {
3435
_phantom: PhantomData<P>,
3536
// PhantomData<*const ()> makes Cap !Send + !Sync
@@ -68,6 +69,7 @@ impl<P: Permission> Cap<P> {
6869
///
6970
/// This is an explicit opt-in — you're acknowledging that this capability
7071
/// will be used in a multi-threaded context (e.g., passed into `tokio::spawn`).
72+
#[must_use = "make_send consumes the original Cap and returns a SendCap"]
7173
pub fn make_send(self) -> SendCap<P> {
7274
SendCap {
7375
_phantom: PhantomData,
@@ -100,6 +102,7 @@ impl<P: Permission> Clone for Cap<P> {
100102
/// // use cap in this thread
101103
/// }).join().unwrap();
102104
/// ```
105+
#[must_use = "capability tokens are proof of permission — discarding one wastes a grant"]
103106
pub struct SendCap<P: Permission> {
104107
_phantom: PhantomData<P>,
105108
}

crates/capsec-core/src/cap_provider.rs

Lines changed: 209 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use crate::permission::Permission;
1414
pub trait CapProvider<P: Permission> {
1515
/// Provides a `Cap<P>` for the given target, or returns an error if the
1616
/// target is outside the capability's scope.
17+
#[must_use = "ignoring a capability check silently discards scope violations"]
1718
fn provide_cap(&self, target: &str) -> Result<Cap<P>, CapSecError>;
1819
}
1920

@@ -63,7 +64,7 @@ impl<P: Permission> CapProvider<P> for crate::cap::SendCap<P> {
6364
}
6465
}
6566

66-
// ── Subsumption ─────────────────────────────────────────────────────
67+
// ── Subsumption (Cap) ────────────────────────────────────────────────
6768

6869
macro_rules! impl_cap_provider_subsumes {
6970
($super:ty => $($sub:ty),+) => {
@@ -82,7 +83,24 @@ use crate::permission::*;
8283
impl_cap_provider_subsumes!(FsAll => FsRead, FsWrite);
8384
impl_cap_provider_subsumes!(NetAll => NetConnect, NetBind);
8485

85-
// ── Ambient ─────────────────────────────────────────────────────────
86+
// ── Subsumption (SendCap) ───────────────────────────────────────────
87+
88+
macro_rules! impl_cap_provider_sendcap_subsumes {
89+
($super:ty => $($sub:ty),+) => {
90+
$(
91+
impl CapProvider<$sub> for crate::cap::SendCap<$super> {
92+
fn provide_cap(&self, _target: &str) -> Result<Cap<$sub>, CapSecError> {
93+
Ok(Cap::new())
94+
}
95+
}
96+
)+
97+
}
98+
}
99+
100+
impl_cap_provider_sendcap_subsumes!(FsAll => FsRead, FsWrite);
101+
impl_cap_provider_sendcap_subsumes!(NetAll => NetConnect, NetBind);
102+
103+
// ── Ambient (Cap) ───────────────────────────────────────────────────
86104

87105
macro_rules! impl_cap_provider_ambient {
88106
($($perm:ty),+) => {
@@ -100,7 +118,25 @@ impl_cap_provider_ambient!(
100118
FsRead, FsWrite, FsAll, NetConnect, NetBind, NetAll, EnvRead, EnvWrite, Spawn
101119
);
102120

103-
// ── Tuples ──────────────────────────────────────────────────────────
121+
// ── Ambient (SendCap) ───────────────────────────────────────────────
122+
123+
macro_rules! impl_cap_provider_sendcap_ambient {
124+
($($perm:ty),+) => {
125+
$(
126+
impl CapProvider<$perm> for crate::cap::SendCap<Ambient> {
127+
fn provide_cap(&self, _target: &str) -> Result<Cap<$perm>, CapSecError> {
128+
Ok(Cap::new())
129+
}
130+
}
131+
)+
132+
}
133+
}
134+
135+
impl_cap_provider_sendcap_ambient!(
136+
FsRead, FsWrite, FsAll, NetConnect, NetBind, NetAll, EnvRead, EnvWrite, Spawn
137+
);
138+
139+
// ── Tuples (Cap) ────────────────────────────────────────────────────
104140

105141
macro_rules! impl_cap_provider_tuple_first {
106142
([$($a:ident),+]; $all:tt) => {
@@ -145,6 +181,101 @@ impl_cap_provider_tuple_second!(
145181
FsRead, FsWrite, FsAll, NetConnect, NetBind, NetAll, EnvRead, EnvWrite, Spawn, Ambient
146182
);
147183

184+
// ── Tuples (SendCap) ────────────────────────────────────────────────
185+
186+
macro_rules! impl_cap_provider_sendcap_tuple_first {
187+
([$($a:ident),+]; $all:tt) => {
188+
$( impl_cap_provider_sendcap_tuple_first!(@inner $a; $all); )+
189+
};
190+
(@inner $a:ident; [$($b:ident),+]) => {
191+
$(
192+
impl CapProvider<$a> for crate::cap::SendCap<($a, $b)> {
193+
fn provide_cap(&self, _target: &str) -> Result<Cap<$a>, CapSecError> {
194+
Ok(Cap::new())
195+
}
196+
}
197+
)+
198+
};
199+
}
200+
201+
macro_rules! impl_cap_provider_sendcap_tuple_second {
202+
($first:ident $(, $rest:ident)+) => {
203+
$(
204+
impl CapProvider<$first> for crate::cap::SendCap<($rest, $first)> {
205+
fn provide_cap(&self, _target: &str) -> Result<Cap<$first>, CapSecError> {
206+
Ok(Cap::new())
207+
}
208+
}
209+
impl CapProvider<$rest> for crate::cap::SendCap<($first, $rest)> {
210+
fn provide_cap(&self, _target: &str) -> Result<Cap<$rest>, CapSecError> {
211+
Ok(Cap::new())
212+
}
213+
}
214+
)+
215+
impl_cap_provider_sendcap_tuple_second!($($rest),+);
216+
};
217+
($single:ident) => {};
218+
}
219+
220+
impl_cap_provider_sendcap_tuple_first!(
221+
[FsRead, FsWrite, FsAll, NetConnect, NetBind, NetAll, EnvRead, EnvWrite, Spawn, Ambient];
222+
[FsRead, FsWrite, FsAll, NetConnect, NetBind, NetAll, EnvRead, EnvWrite, Spawn, Ambient]
223+
);
224+
225+
impl_cap_provider_sendcap_tuple_second!(
226+
FsRead, FsWrite, FsAll, NetConnect, NetBind, NetAll, EnvRead, EnvWrite, Spawn, Ambient
227+
);
228+
229+
// ── Runtime / Prescript types ───────────────────────────────────────
230+
231+
impl<P: Permission> CapProvider<P> for crate::runtime::RuntimeCap<P> {
232+
fn provide_cap(&self, _target: &str) -> Result<Cap<P>, CapSecError> {
233+
self.try_cap()
234+
}
235+
}
236+
237+
impl<P: Permission> CapProvider<P> for crate::runtime::RuntimeSendCap<P> {
238+
fn provide_cap(&self, _target: &str) -> Result<Cap<P>, CapSecError> {
239+
self.try_cap()
240+
}
241+
}
242+
243+
impl<P: Permission> CapProvider<P> for crate::runtime::TimedCap<P> {
244+
fn provide_cap(&self, _target: &str) -> Result<Cap<P>, CapSecError> {
245+
self.try_cap()
246+
}
247+
}
248+
249+
impl<P: Permission> CapProvider<P> for crate::runtime::TimedSendCap<P> {
250+
fn provide_cap(&self, _target: &str) -> Result<Cap<P>, CapSecError> {
251+
self.try_cap()
252+
}
253+
}
254+
255+
impl<P: Permission> CapProvider<P> for crate::prescript::LoggedCap<P> {
256+
fn provide_cap(&self, _target: &str) -> Result<Cap<P>, CapSecError> {
257+
self.try_cap()
258+
}
259+
}
260+
261+
impl<P: Permission> CapProvider<P> for crate::prescript::LoggedSendCap<P> {
262+
fn provide_cap(&self, _target: &str) -> Result<Cap<P>, CapSecError> {
263+
self.try_cap()
264+
}
265+
}
266+
267+
impl<P: Permission> CapProvider<P> for crate::prescript::DualKeyCap<P> {
268+
fn provide_cap(&self, _target: &str) -> Result<Cap<P>, CapSecError> {
269+
self.try_cap()
270+
}
271+
}
272+
273+
impl<P: Permission> CapProvider<P> for crate::prescript::DualKeySendCap<P> {
274+
fn provide_cap(&self, _target: &str) -> Result<Cap<P>, CapSecError> {
275+
self.try_cap()
276+
}
277+
}
278+
148279
#[cfg(test)]
149280
mod tests {
150281
use super::*;
@@ -186,4 +317,79 @@ mod tests {
186317
assert!(CapProvider::<FsRead>::provide_cap(&cap, "/any").is_ok());
187318
assert!(CapProvider::<NetConnect>::provide_cap(&cap, "host").is_ok());
188319
}
320+
321+
#[test]
322+
fn sendcap_subsumption_provides() {
323+
let root = crate::root::test_root();
324+
let cap = root.grant::<FsAll>().make_send();
325+
let result: Result<Cap<FsRead>, _> = cap.provide_cap("/any");
326+
assert!(result.is_ok());
327+
}
328+
329+
#[test]
330+
fn sendcap_ambient_provides() {
331+
let root = crate::root::test_root();
332+
let cap = root.grant::<Ambient>().make_send();
333+
let result: Result<Cap<FsRead>, _> = cap.provide_cap("/any");
334+
assert!(result.is_ok());
335+
}
336+
337+
#[test]
338+
fn sendcap_tuple_provides() {
339+
let root = crate::root::test_root();
340+
let cap = root.grant::<(FsRead, NetConnect)>().make_send();
341+
assert!(CapProvider::<FsRead>::provide_cap(&cap, "/any").is_ok());
342+
assert!(CapProvider::<NetConnect>::provide_cap(&cap, "host").is_ok());
343+
}
344+
345+
#[test]
346+
fn runtime_cap_provides() {
347+
let root = crate::root::test_root();
348+
let cap = root.grant::<FsRead>();
349+
let (rcap, _revoker) = crate::runtime::RuntimeCap::new(cap);
350+
assert!(rcap.provide_cap("/any").is_ok());
351+
}
352+
353+
#[test]
354+
fn runtime_cap_provides_fails_after_revocation() {
355+
let root = crate::root::test_root();
356+
let cap = root.grant::<FsRead>();
357+
let (rcap, revoker) = crate::runtime::RuntimeCap::new(cap);
358+
revoker.revoke();
359+
assert!(rcap.provide_cap("/any").is_err());
360+
}
361+
362+
#[test]
363+
fn timed_cap_provides() {
364+
let root = crate::root::test_root();
365+
let cap = root.grant::<FsRead>();
366+
let tcap = crate::runtime::TimedCap::new(cap, std::time::Duration::from_secs(60));
367+
assert!(tcap.provide_cap("/any").is_ok());
368+
}
369+
370+
#[test]
371+
fn logged_cap_provides() {
372+
let root = crate::root::test_root();
373+
let cap = root.grant::<FsRead>();
374+
let lcap = crate::prescript::LoggedCap::new(cap);
375+
assert!(lcap.provide_cap("/any").is_ok());
376+
}
377+
378+
#[test]
379+
fn dual_key_cap_provides() {
380+
let root = crate::root::test_root();
381+
let cap = root.grant::<FsRead>();
382+
let (dcap, a, b) = crate::prescript::DualKeyCap::new(cap);
383+
a.approve();
384+
b.approve();
385+
assert!(dcap.provide_cap("/any").is_ok());
386+
}
387+
388+
#[test]
389+
fn dual_key_cap_provides_fails_without_approvals() {
390+
let root = crate::root::test_root();
391+
let cap = root.grant::<FsRead>();
392+
let (dcap, _a, _b) = crate::prescript::DualKeyCap::new(cap);
393+
assert!(dcap.provide_cap("/any").is_err());
394+
}
189395
}

0 commit comments

Comments
 (0)