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
24 changes: 21 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
```
capsec/
├── crates/
│ ├── capsec-core/ # Zero-cost capability tokens, permission traits, sealed Has<P>
│ ├── capsec-macro/ # #[requires] and #[deny] proc macros
│ ├── capsec-core/ # Zero-cost capability tokens, permission traits, Has<P>
│ ├── capsec-macro/ # #[requires], #[deny], #[main], #[context] proc macros
│ ├── capsec-std/ # Capability-gated wrappers for std::fs, std::net, etc.
│ ├── capsec/ # Facade crate — re-exports everything, owns examples
│ ├── cargo-capsec/ # Static audit CLI tool
Expand Down Expand Up @@ -155,5 +155,23 @@ TRYBUILD=overwrite cargo test -p capsec --test compile_tests
- **One concern per PR.** A new authority pattern, a bug fix, or a refactor — not all three.
- **Include tests.** New authority patterns need integration tests. New type-system features need compile-fail tests.
- **Run `cargo capsec audit`** against the repo itself before submitting — capsec dogfoods its own tool.
- **Keep the security model intact.** `Cap<P>` must remain unforgeable, `!Send`, and sealed. Any change that weakens these guarantees needs discussion in an issue first.
- **Keep the security model intact.** `Cap<P>` must remain unforgeable and `!Send`. `Permission` must remain sealed. `Cap::new()` must remain `pub(crate)`. Any change that weakens these guarantees needs discussion in an issue first.
- **Update docs** if you change public API. The facade crate's `lib.rs` doc comments and crate READMEs should stay current.

## Context pattern and macros

capsec provides three ergonomic macros that work together:

| Macro | Purpose |
|-------|---------|
| `#[capsec::context]` | Generates `Has<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 |

When developing macros in `capsec-macro`:

- All generated code uses fully qualified `capsec_core::*` paths (not `capsec::*`)
- Permission type validation must stay in sync with `capsec-core/src/permission.rs`
- The `resolve.rs` module maps shorthand paths (`fs::read`) to full types
- Add compile-fail tests in `capsec/tests/compile_fail/` for error cases
- Add runtime tests in `capsec-tests/tests/type_system.rs` for happy paths
28 changes: 28 additions & 0 deletions Cargo.lock

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

40 changes: 24 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[![CI](https://github.com/bordumb/capsec/actions/workflows/ci.yml/badge.svg)](https://github.com/bordumb/capsec/actions/workflows/ci.yml)
[![crates.io](https://img.shields.io/crates/v/capsec.svg)](https://crates.io/crates/capsec)
[![docs.rs](https://docs.rs/capsec/badge.svg)](https://docs.rs/capsec)
[![License: MIT OR Apache-2.0](https://img.shields.io/crates/l/capsec.svg)](LICENSE)
[![License: Apache-2.0](https://img.shields.io/crates/l/capsec.svg)](LICENSE)

Capability-based security tooling for Rust.

Expand All @@ -26,6 +26,9 @@ Scans Rust source for ambient authority (filesystem, network, env, process) and

```bash
cargo install cargo-capsec

# Or from source:
cargo install --path crates/cargo-capsec
```

### Run
Expand Down Expand Up @@ -105,30 +108,33 @@ Functions declare their I/O requirements in the type signature. The compiler enf
```rust
use capsec::prelude::*;

// This function CANNOT do I/O — it has no capability token.
// Adding std::fs::read() here would require a Cap<FsRead> parameter,
// which the compiler would demand.
pub fn process_csv(input: &[u8]) -> Vec<Vec<String>> {
parse(input)
// Define a context with exactly the permissions your app needs.
// The macro generates Cap fields, constructor, and Has<P> impls.
#[capsec::context]
struct AppCtx {
fs: FsRead,
net: NetConnect,
}

// This function declares it needs filesystem read access.
// The caller must provide proof via a Cap<FsRead> token.
// 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> {
capsec::fs::read_to_string(path, cap)
}

// In main — the single point of authority:
fn main() {
let root = capsec::root();
let fs_cap = root.grant::<FsRead>();
// Intermediate functions take a single context reference — not N separate caps.
pub fn app_logic(ctx: &AppCtx) -> Result<String, CapSecError> {
load_config("/etc/app/config.toml", ctx) // ctx satisfies Has<FsRead>
}

let config = load_config("/etc/app/config.toml", &fs_cap).unwrap();
let result = process_csv(input); // no cap needed — pure computation
// #[capsec::main] injects the capability root automatically.
#[capsec::main]
fn main(root: CapRoot) {
let ctx = AppCtx::new(&root);
let config = app_logic(&ctx).unwrap();
}
```

Every capability traces back to `root.grant()`. If a function uses capsec wrappers (like `capsec::fs::read_to_string`) without being given a `Cap<FsRead>`, the code doesn't compile. The audit tool catches code that bypasses capsec wrappers entirely — calling `std::fs` directly, using FFI, or hiding I/O behind re-exports.
Every capability traces back to `CapRoot`. If a function uses capsec wrappers (like `capsec::fs::read_to_string`) without being given a `Cap<FsRead>`, the code doesn't compile. The audit tool catches code that bypasses capsec wrappers entirely — calling `std::fs` directly, using FFI, or hiding I/O behind re-exports.

### What the compiler actually says

Expand Down Expand Up @@ -200,6 +206,8 @@ These are real `rustc` errors — no custom error framework, no runtime panics.

capsec protects against **cooperative safe Rust** — code that uses capsec wrappers cannot exceed its declared permissions, and the compiler enforces this at zero runtime cost.

The `Has<P>` trait is open for implementation — custom context structs can implement it to delegate capability access. Security is maintained because `Cap::new()` is `pub(crate)`: no external code can forge a `Cap<P>` in safe Rust. The `Permission` trait remains sealed — external crates cannot invent new permission types.

What capsec **does not** protect against:

- **`unsafe` code** that forges capability tokens via `transmute`, `MaybeUninit`, or pointer tricks. The type system is sound only within safe Rust. (The adversarial test suite in `capsec-tests/tests/type_system.rs` documents these attacks and confirms they require `unsafe`.)
Expand All @@ -226,4 +234,4 @@ For the full catalog of known evasion vectors and how each tool handles them, se

## License

MIT OR Apache-2.0
Apache-2.0
12 changes: 9 additions & 3 deletions crates/capsec-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ This is the foundation crate of the [capsec](https://github.com/bordumb/capsec)

- **`Permission`** — sealed marker trait for capability categories (`FsRead`, `NetConnect`, `Spawn`, etc.)
- **`Cap<P>`** — zero-sized proof token that the holder has permission `P`
- **`Has<P>`** — trait bound for declaring capability requirements in function signatures
- **`CapRoot`** — singleton factory for granting capabilities
- **`SendCap<P>`** — thread-safe variant of `Cap<P>` (`Send + Sync`)
- **`Has<P>`** — trait bound for declaring capability requirements. Open for implementation — custom context structs can implement `Has<P>` to delegate capability access.
- **`CapRoot`** — singleton factory for granting capabilities, with convenience methods (`fs_read()`, `net_connect()`, etc.)
- **`Attenuated<P, S>`** — scope-restricted capabilities (`DirScope`, `HostScope`)

All types are zero-sized at runtime. No overhead.

Security is maintained because `Cap::new()` is `pub(crate)` — no external code can forge a `Cap<P>` in safe Rust. The `Permission` trait remains sealed.

## Example

```rust,ignore
Expand All @@ -22,7 +25,10 @@ use capsec_core::permission::FsRead;
use capsec_core::has::Has;

let root = test_root();

// Turbofish or convenience method — both work:
let cap = root.grant::<FsRead>();
let cap = root.fs_read();

fn needs_fs(cap: &impl Has<FsRead>) {
// can only be called with proof of FsRead permission
Expand All @@ -33,4 +39,4 @@ needs_fs(&cap);

## License

MIT OR Apache-2.0
Apache-2.0
42 changes: 16 additions & 26 deletions crates/capsec-core/src/has.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,17 @@
//! `Cap<FsAll>` satisfies `Has<FsRead>` and `Has<FsWrite>` because `FsAll`
//! subsumes both. `Cap<Ambient>` satisfies `Has<P>` for every permission.

use crate::cap::Cap;
use crate::cap::{Cap, SendCap};
use crate::permission::*;

/// Proof that a capability token includes permission `P`.
///
/// This trait is **sealed** — it cannot be implemented outside `capsec-core`.
/// Use [`CapRoot::grant()`](crate::root::CapRoot::grant) to obtain capability tokens.
/// This trait is open for implementation — custom context structs can implement
/// `Has<P>` to delegate capability access. Security is maintained because
/// `Cap::new()` is `pub(crate)`: no external code can forge a `Cap<P>` in safe Rust.
///
/// Use [`CapRoot::grant()`](crate::root::CapRoot::grant) to obtain capability tokens,
/// or implement `Has<P>` on your own structs using the `#[capsec::context]` macro.
///
/// # Example
///
Expand All @@ -54,28 +58,11 @@ use crate::permission::*;
/// let cap = root.grant::<FsRead>();
/// needs_fs(&cap);
/// ```
#[allow(private_bounds)]
pub trait Has<P: Permission>: sealed::Sealed<P> {
pub trait Has<P: Permission> {
/// Returns a new `Cap<P>` proving the permission is available.
fn cap_ref(&self) -> Cap<P>;
}

// Sealed supertrait for Has<P> — prevents external implementations.
//
// This is separate from the `sealed` module in `permission.rs`, which seals the
// Permission trait (controlling which types can be permissions). This module seals
// the Has trait (controlling which types can claim to hold a permission).

mod sealed {
use crate::cap::Cap;
use crate::permission::Permission;

pub trait Sealed<P: Permission> {}

// Direct: Cap<P> satisfies Has<P> for any permission P
impl<P: Permission> Sealed<P> for Cap<P> {}
}

// Direct: Cap<P> implements Has<P>

impl<P: Permission> Has<P> for Cap<P> {
Expand All @@ -84,12 +71,19 @@ impl<P: Permission> Has<P> for Cap<P> {
}
}

// SendCap<P> delegates to Has<P>

impl<P: Permission> Has<P> for SendCap<P> {
fn cap_ref(&self) -> Cap<P> {
self.as_cap()
}
}

// Subsumption: FsAll, NetAll

macro_rules! impl_subsumes {
($super:ty => $($sub:ty),+) => {
$(
impl sealed::Sealed<$sub> for Cap<$super> {}
impl Has<$sub> for Cap<$super> {
fn cap_ref(&self) -> Cap<$sub> { Cap::new() }
}
Expand All @@ -109,7 +103,6 @@ impl_subsumes!(NetAll => NetConnect, NetBind);
macro_rules! impl_ambient {
($($perm:ty),+) => {
$(
impl sealed::Sealed<$perm> for Cap<Ambient> {}
impl Has<$perm> for Cap<Ambient> {
fn cap_ref(&self) -> Cap<$perm> { Cap::new() }
}
Expand Down Expand Up @@ -137,7 +130,6 @@ macro_rules! impl_tuple_has_first {
};
(@inner $a:ident; [$($b:ident),+]) => {
$(
impl sealed::Sealed<$a> for Cap<($a, $b)> {}
impl Has<$a> for Cap<($a, $b)> {
fn cap_ref(&self) -> Cap<$a> { Cap::new() }
}
Expand All @@ -148,11 +140,9 @@ macro_rules! impl_tuple_has_first {
macro_rules! impl_tuple_has_second {
($first:ident $(, $rest:ident)+) => {
$(
impl sealed::Sealed<$first> for Cap<($rest, $first)> {}
impl Has<$first> for Cap<($rest, $first)> {
fn cap_ref(&self) -> Cap<$first> { Cap::new() }
}
impl sealed::Sealed<$rest> for Cap<($first, $rest)> {}
impl Has<$rest> for Cap<($first, $rest)> {
fn cap_ref(&self) -> Cap<$rest> { Cap::new() }
}
Expand Down
Loading
Loading