diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..66322da --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,88 @@ +# Security Model + +capsec enforces I/O permissions at compile time via a zero-cost capability type system. This document describes what it protects against, what it doesn't, and where the boundaries are. + +## The security boundary: `Cap::new()` is `pub(crate)` + +The only way to construct a `Cap
` in safe Rust is through `CapRoot::grant()`. The constructor `Cap::new()` is `pub(crate)` — external crates cannot call it. + +```rust +// This is the entire security gate: +pub(crate) fn new() -> Self { ... } +``` + +Every capability in a capsec program traces back to a `CapRoot::grant()` call. `CapRoot` is a singleton — `root()` panics if called twice, ensuring a single point of authority. + +## `Has
` is intentionally open + +The `Has
` trait is open for external implementation. This is by design — the `#[capsec::context]` macro generates `impl Has
` on user-defined structs to enable capability threading through call stacks. + +This is safe because `Has
` requires returning a `Cap
`:
+
+```rust
+pub trait Has ;
+}
+```
+
+Any implementation must produce a `Cap `. Since `Cap::new()` is `pub(crate)`, the only way to satisfy this in safe Rust is to hold a real `Cap ` obtained from `CapRoot::grant()`.
+
+## Enforcement: type-witnessed proofs
+
+All `capsec-std` and `capsec-tokio` wrapper functions extract a typed proof before executing I/O:
+
+```rust
+pub fn read(path: impl AsRef ` implementation diverges (panics, loops, calls `process::exit`), the divergence fires at the `_proof` line — the I/O on the next line never executes.
+
+The adversarial test suite (`capsec-tests/tests/type_system.rs`, section L) proves this with a `PanicForge` impl that panics in `cap_ref()` — the test confirms the panic fires before `std::fs::read` runs.
+
+## What capsec does NOT protect against
+
+### `unsafe` code
+
+`transmute`, `MaybeUninit`, and `ptr::read` can forge a `Cap ` because `Cap` is a zero-sized type. All three attacks are documented and tested in `capsec-tests/tests/type_system.rs` (section C). This is expected — capsec's threat model is safe Rust only.
+
+### Direct `std` / `tokio` calls
+
+A function can always call `std::fs::read()` or `tokio::fs::read()` directly without a capability token. The Rust compiler won't stop it — capsec wrappers are opt-in replacements, not mandatory.
+
+This is where `cargo capsec audit` comes in. The audit tool statically scans source code for ambient authority calls and reports them. Functions annotated with `#[capsec::deny(all)]` that contain I/O calls are promoted to critical risk.
+
+### FFI and inline assembly
+
+`extern` blocks and inline `asm!` interact with the OS directly. The audit tool flags `extern` blocks but cannot analyze what foreign code does.
+
+### Proc-macro-generated code
+
+Code generated by procedural macros is invisible to the static scanner. This is inherent to syntax-level tooling.
+
+## Audit tool scope
+
+`cargo capsec audit` is an AST-level heuristic scanner, not a data flow analyzer. It:
+
+- Detects qualified calls (`std::fs::read`, `TcpStream::connect`, `Command::new`)
+- Expands import aliases (`use std::fs::read as load; load(...)`)
+- Handles glob imports (`use std::fs::*; read(...)`)
+- Uses contextual method matching (`.output()` only flags when `Command::new` is in the same function)
+- Flags `extern` blocks as FFI findings
+
+Known blind spots (documented in `capsec-tests/tests/audit_evasion.rs`):
+
+- Function pointers and closures that hide the call target
+- `include!()` directives that pull in code from other files
+- Module re-exports across crate boundaries
+- Dependency re-exports (a crate wrapping `std::fs` under a different name)
+- `libc` / `nix` crate calls (not in the pattern registry)
+- `cfg`-conditional code that may or may not compile
+
+## Reporting vulnerabilities
+
+If you discover a security issue in capsec, please email the maintainers directly rather than opening a public issue. Contact: [open an issue with the `security` label](https://github.com/bordumb/capsec/issues/new?labels=security).
+
+For issues that are not security-sensitive (e.g., audit tool false negatives, documentation gaps), please open a regular GitHub issue.
diff --git a/crates/capsec-tests/tests/type_system.rs b/crates/capsec-tests/tests/type_system.rs
index 4b49950..e9ac0ef 100644
--- a/crates/capsec-tests/tests/type_system.rs
+++ b/crates/capsec-tests/tests/type_system.rs
@@ -432,3 +432,50 @@ fn create_returns_writable_file() {
std::io::Write::write_all(&mut file, b"test").unwrap();
std::fs::remove_file(&path).ok();
}
+
+// ============================================================================
+// L. FORGERY DEFENSE — proving the _proof pattern stops diverging Has impls
+// ============================================================================
+//
+// The _proof pattern (`let _proof: Cap = cap.cap_ref()`) forces cap_ref()
+// to evaluate and return before any I/O executes. If an external impl of
+// Has diverges (panic, loop, process::exit), the divergence fires at the
+// _proof line — the I/O on the next line never runs.
+//
+// This is the enforcement mechanism that makes open Has safe: you can
+// implement the trait, but you can't skip the proof extraction.
+
+/// A malicious Has impl (panic) fires BEFORE std::fs::read executes.
+/// If the _proof pattern were missing or incorrect, the I/O would run first.
+#[test]
+#[should_panic(expected = "forged capability")]
+fn forged_has_impl_panics_before_io_executes() {
+ let _ = capsec_std::fs::read("/dev/null", &PanicForge);
+}
+
+/// A malicious Has being open does not enable I/O bypass via divergence.
+#[allow(dead_code)]
+struct LoopForge;
+
+#[allow(dead_code)]
+impl Has