Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ capsec provides four macros that work together:

| Macro | Purpose |
|-------|---------|
| `#[capsec::context]` | Generates `Has<P>` impls on a struct, turning it into a capability context |
| `#[capsec::context]` | Generates `Has<P>` and `CapProvider<P>` impls on a struct, turning it into a capability context |
| `#[capsec::main]` | Injects `CapRoot` creation into a function entry point |
| `#[capsec::requires]` | Validates that a function's parameters satisfy declared permissions |
| `#[capsec::deny]` | Marks a function as capability-free; violations are promoted to critical by the audit tool |
Expand Down
16 changes: 8 additions & 8 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 5 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ exclude = [
]

[workspace.package]
version = "0.1.9"
version = "0.2.0"
edition = "2024"
rust-version = "1.85.0"
readme = "README.md"
Expand Down Expand Up @@ -51,7 +51,7 @@ trybuild = "1"
insta = { version = "1", features = ["yaml"] }

# Internal crates
capsec-core = { path = "crates/capsec-core", version = "0.1.0" }
capsec-macro = { path = "crates/capsec-macro", version = "0.1.0" }
capsec-std = { path = "crates/capsec-std", version = "0.1.0" }
capsec-tokio = { path = "crates/capsec-tokio", version = "0.1.0" }
capsec-core = { path = "crates/capsec-core", version = "0.2.0" }
capsec-macro = { path = "crates/capsec-macro", version = "0.2.0" }
capsec-std = { path = "crates/capsec-std", version = "0.2.0" }
capsec-tokio = { path = "crates/capsec-tokio", version = "0.2.0" }
24 changes: 12 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ cargo capsec audit
capsec fills that gap with three layers:

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.
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.
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.
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.

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.
Expand Down Expand Up @@ -120,15 +120,15 @@ Functions declare their I/O requirements in the type signature. The compiler enf
use capsec::prelude::*;

// Define a context with exactly the permissions your app needs.
// The macro generates Cap fields, constructor, and Has<P> impls.
// The macro generates Cap fields, constructor, Has<P> impls, and CapProvider<P> impls.
#[capsec::context]
struct AppCtx {
fs: FsRead,
net: NetConnect,
}

// Leaf functions take &impl Has<P> — works with raw caps AND context structs.
pub fn load_config(path: &str, cap: &impl Has<FsRead>) -> Result<String, CapSecError> {
// Leaf functions take &impl CapProvider<P> — works with raw caps, context structs, AND scoped caps.
pub fn load_config(path: &str, cap: &impl CapProvider<FsRead>) -> Result<String, CapSecError> {
capsec::fs::read_to_string(path, cap)
}

Expand Down Expand Up @@ -157,11 +157,11 @@ let _ = capsec::fs::read_to_string("/etc/passwd", &net_cap);
```

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

```rust
let fs_all = root.grant::<FsAll>();
needs_net(&fs_all); // fn needs_net(_: &impl Has<NetConnect>) {}
needs_net(&fs_all); // fn needs_net(_: &impl CapProvider<NetConnect>) {}
```

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

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

### Key properties

- `RuntimeCap`, `TimedCap`, `LoggedCap`, and `DualKeyCap` do **not** implement `Has<P>` — fallibility is explicit via `try_cap()` at every call site
- `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()`
- All are `!Send` by default — use `make_send()` to opt into cross-thread transfer
- Cloning a `RuntimeCap` shares the revocation flag — revoking one revokes all clones
- Cloning a `LoggedCap` shares the audit log — entries from any clone appear in the same log
Expand Down Expand Up @@ -379,7 +379,7 @@ capsec's design draws from three foundational papers in capability-based securit

- **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.

- **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.
- **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()`.

---

Expand Down
1 change: 1 addition & 0 deletions crates/capsec-core/src/attenuate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ impl<P: Permission> Cap<P> {

impl<P: Permission, S: Scope> Attenuated<P, S> {
/// Checks whether `target` is within this capability's scope.
#[must_use = "ignoring a scope check silently discards scope violations"]
pub fn check(&self, target: &str) -> Result<(), CapSecError> {
self.scope.check(target)
}
Expand Down
3 changes: 3 additions & 0 deletions crates/capsec-core/src/cap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ use std::marker::PhantomData;
/// // cap is a proof token — zero bytes at runtime
/// assert_eq!(std::mem::size_of_val(&cap), 0);
/// ```
#[must_use = "capability tokens are proof of permission — discarding one wastes a grant"]
pub struct Cap<P: Permission> {
_phantom: PhantomData<P>,
// PhantomData<*const ()> makes Cap !Send + !Sync
Expand Down Expand Up @@ -68,6 +69,7 @@ impl<P: Permission> Cap<P> {
///
/// This is an explicit opt-in — you're acknowledging that this capability
/// will be used in a multi-threaded context (e.g., passed into `tokio::spawn`).
#[must_use = "make_send consumes the original Cap and returns a SendCap"]
pub fn make_send(self) -> SendCap<P> {
SendCap {
_phantom: PhantomData,
Expand Down Expand Up @@ -100,6 +102,7 @@ impl<P: Permission> Clone for Cap<P> {
/// // use cap in this thread
/// }).join().unwrap();
/// ```
#[must_use = "capability tokens are proof of permission — discarding one wastes a grant"]
pub struct SendCap<P: Permission> {
_phantom: PhantomData<P>,
}
Expand Down
Loading
Loading